Compare commits
31
Commits
76075223c7
..
v2.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68e08c2413 | ||
|
|
d42f6368fe | ||
|
|
c68e864159 | ||
|
|
c7ba3d2ccf | ||
|
|
4d336bbf35 | ||
|
|
36596598ef | ||
|
|
b1ea14df3e | ||
|
|
de84d1b677 | ||
|
|
65bac15257 | ||
|
|
b0ef503485 | ||
|
|
bd2bf667c5 | ||
|
|
ba7cec7672 | ||
|
|
56cc173f65 | ||
|
|
b93af30750 | ||
|
|
b9c7762463 | ||
|
|
1929ffd2da | ||
|
|
123ece2fb3 | ||
|
|
9385c77ac7 | ||
|
|
54d7bd80ba | ||
|
|
75a13c82f6 | ||
|
|
79c9089122 | ||
|
|
ede3f06689 | ||
|
|
b0d6d08239 | ||
|
|
f7cc27908c | ||
|
|
e7ebc667b3 | ||
|
|
64ffb9f35a | ||
|
|
d02765d595 | ||
|
|
311e7cd22c | ||
|
|
2540e50fcc | ||
|
|
a85bdbed42 | ||
|
|
b6e68e9a2b |
No files matched your search
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "holocron",
|
||||
"description": "AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.5",
|
||||
"owner": {
|
||||
"name": "Defame1297",
|
||||
"email": "[email protected]",
|
||||
@@ -11,28 +11,28 @@
|
||||
{
|
||||
"name": "kyberforge",
|
||||
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.0",
|
||||
"category": "Developer Tools",
|
||||
"source": "./plugins/kyberforge"
|
||||
},
|
||||
{
|
||||
"name": "bin",
|
||||
"description": "A place for things to be binned",
|
||||
"version": "1.1.3",
|
||||
"description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.",
|
||||
"version": "1.1.5",
|
||||
"category": "Utilities",
|
||||
"source": "./plugins/bin"
|
||||
},
|
||||
{
|
||||
"name": "git",
|
||||
"description": "Skills for working with Git — conventional commits, branch management, pull requests, and feature flow.",
|
||||
"version": "1.3.3",
|
||||
"description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.",
|
||||
"version": "1.3.5",
|
||||
"category": "Version Control",
|
||||
"source": "./plugins/git"
|
||||
},
|
||||
{
|
||||
"name": "gitea",
|
||||
"description": "Skills for managing Gitea repositories — issues, pull requests, milestones, releases, and wikis.",
|
||||
"version": "1.3.4",
|
||||
"description": "Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.",
|
||||
"version": "1.3.6",
|
||||
"category": "Version Control",
|
||||
"source": "./plugins/gitea"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "holocron",
|
||||
"description": "AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.5",
|
||||
"owner": {
|
||||
"name": "Defame1297",
|
||||
"email": "[email protected]",
|
||||
@@ -11,28 +11,28 @@
|
||||
{
|
||||
"name": "kyberforge",
|
||||
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.0",
|
||||
"category": "Developer Tools",
|
||||
"source": "./plugins/kyberforge"
|
||||
},
|
||||
{
|
||||
"name": "bin",
|
||||
"description": "A place for things to be binned",
|
||||
"version": "1.1.3",
|
||||
"description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.",
|
||||
"version": "1.1.5",
|
||||
"category": "Utilities",
|
||||
"source": "./plugins/bin"
|
||||
},
|
||||
{
|
||||
"name": "git",
|
||||
"description": "Skills for working with Git — conventional commits, branch management, pull requests, and feature flow.",
|
||||
"version": "1.3.3",
|
||||
"description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.",
|
||||
"version": "1.3.5",
|
||||
"category": "Version Control",
|
||||
"source": "./plugins/git"
|
||||
},
|
||||
{
|
||||
"name": "gitea",
|
||||
"description": "Skills for managing Gitea repositories — issues, pull requests, milestones, releases, and wikis.",
|
||||
"version": "1.3.4",
|
||||
"description": "Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.",
|
||||
"version": "1.3.6",
|
||||
"category": "Version Control",
|
||||
"source": "./plugins/gitea"
|
||||
},
|
||||
|
||||
@@ -207,7 +207,7 @@ repos:
|
||||
# pre-commit prints nothing at all for a passing hook, so without this
|
||||
# the opt-out reinstated exactly the silent vacuous pass the script was
|
||||
# written to kill, one level up -- the run showed a bare `Passed` and
|
||||
# AGENTS.md's instruction to read that summary line was impossible to
|
||||
# the documented instruction to read that summary line was impossible to
|
||||
# follow in the one situation the opt-out exists for. The script's clean
|
||||
# output is a single line, so this costs one line per push.
|
||||
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
# Working in this repo
|
||||
|
||||
This repo is the global AI development configuration repository — the authoritative source for agent definitions, skills, workflows, and prompts across all projects. Built as a homelab tool intended to scale to professional environments.
|
||||
The global AI development configuration repository — the authoritative source for agent definitions, skills, workflows, and prompts across all projects.
|
||||
|
||||
This file carries only what applies to **every** session. Setup, prerequisites, and test commands are in `README.md`; the reasoning behind each enforcement gate is in `docs/spec/gates.md`.
|
||||
|
||||
## Structure
|
||||
|
||||
- `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`)
|
||||
- `plugins/` — six installable plugin units, each an apm package (`apm.yml` + `.apm/`). Root `apm.yml` declares all six as `dependencies.apm`; `apm install` deploys them into `.claude/skills/` and `.claude/agents/`, both gitignored install output.
|
||||
- `providers/claude-code/` — Claude Code adapter, deployed to `~/.claude/` via `scripts/install.sh`.
|
||||
|
||||
## Edit `.apm/`, never the flat mirror
|
||||
|
||||
Inside a plugin, `plugins/<name>/.apm/` is the **only** hand-edited source for **plugin content** — the skills, agents, commands, instructions, extensions and hooks a host discovers. Everything in a plugin root that mirrors an `.apm/` primitive, plus both `plugin.json` manifests, is generated:
|
||||
`plugins/<name>/.apm/` is the only hand-edited source for plugin content. The flat `plugins/<name>/{skills,agents,commands,instructions,extensions}/` directories, the merged `plugins/<name>/hooks/hooks.json`, and both `plugin.json` manifests are generated — nothing marks them as generated, so check the path before you edit. An edit to the mirror is discarded by the next sync and reported as drift by the `check-plugin-content-sync` pre-push hook.
|
||||
|
||||
- `scripts/sync-plugin-content.sh` generates the flat `plugins/<name>/{skills,agents,commands,instructions,extensions}/` directories and the merged `plugins/<name>/hooks/hooks.json` (ADR-0017)
|
||||
- `apm pack` generates both per-plugin manifests — `plugins/<name>/.claude-plugin/plugin.json` and `plugins/<name>/.github/plugin/plugin.json` — and **two of the three** root marketplace manifests: `.claude-plugin/marketplace.json` (apm's `claude` output profile) and `.agents/plugins/marketplace.json` (its `codex` profile, a differently-shaped file) (ADR-0015)
|
||||
- `scripts/sync-marketplace-mirror.sh` generates the third, `.github/plugin/marketplace.json` — Copilot CLI's legacy manifest path. **No apm output profile targets it**: apm ships exactly two marketplace output profiles, `claude` and `codex` (documented in `plugins/kyberforge/.apm/skills/apm-workflow/references/marketplace.md`). The mirror is a byte-identical copy of `.claude-plugin/marketplace.json`, gated by the `check-marketplace-mirror-sync` pre-push hook. Do not expect `apm pack` to refresh it — that assumption is exactly the drift this pair exists to prevent
|
||||
Not everything in a plugin root is generated. `README.md`, `docs/`, `bin/`, `sources.md`, `.mcp.json` and per-plugin extras are hand-authored there with no `.apm/` source — edit those in place. The rule is per-path, not per-directory. But a file placed *inside* a mirrored directory is deleted on the next sync (`sync_dir` runs `rm -rf` before every copy), so plugin-root documentation goes in `docs/`, never in `hooks/` or `skills/`.
|
||||
|
||||
**A plugin root is not wholly generated.** Material that is not an `.apm/` primitive is hand-authored there and no compiler touches it: `README.md`, `docs/`, `bin/`, `sources.md`, `.mcp.json`, plus per-plugin extras like `plugins/git/config.example.json`, `plugins/gitea/references/` and `plugins/bin/evals/`. Edit those in place — they have no `.apm/` source, and looking for one wastes a search. The rule is per-path, not per-directory: `plugins/<name>/skills/` is generated, `plugins/<name>/docs/` is not. `docs/spec/architecture.md` carries the same carve-out.
|
||||
|
||||
One qualification: "hand-authored, untouched" holds only at the plugin *root*. A file placed **inside** a mirrored directory is destroyed — `sync_dir` runs `rm -rf "$dst"` before every copy, so a `README.md` under `plugins/<name>/hooks/` or `plugins/<name>/skills/` is deleted on the next sync whether or not `.apm/` has a counterpart. Put root-level plugin documentation in `docs/`, never in a mirrored directory.
|
||||
|
||||
Nothing labels a generated file as generated — `plugins/kyberforge/skills/forge/SKILL.md` is byte-identical to its `.apm/` original, with no marker in either. Check the path before you edit. An edit to the mirror is discarded by the next sync and is reported as drift by the `check-plugin-content-sync` pre-push hook, which is the earliest anyone finds out. Details in `docs/spec/architecture.md`.
|
||||
Full model: `docs/spec/architecture.md`.
|
||||
|
||||
## Prefer plugin skills over raw shell
|
||||
|
||||
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, check whether a skill already owns the operation — it usually does:
|
||||
|
||||
- Commits, branches, history, worktrees, remotes → `git-commits`, `git-branches`, `git-history`, `git-worktrees`, `git-remotes`
|
||||
- Pre-commit hook install/config/troubleshooting → `pc-run` / `pc-author`
|
||||
@@ -31,39 +27,33 @@ This repo dogfoods its own plugins. Before shelling out to git, gitea, or lint t
|
||||
- Vale prose linting → `vale-config` / `vale-run`
|
||||
- This repo's own AGENTS.md → `agentsmd-author` / `agentsmd-audit`
|
||||
|
||||
Use the bare, **unnamespaced** names above. 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 has not stopped resolving here, though — `~/.claude.json` still enables `core`, `git`, `gitea`, `kyberforge`, and `lint` at **user** scope, and ADR-0018 left those native installs in place on purpose, converting them being a separate decision with a blast radius beyond this repo. Every skill is therefore live under both names right now, and a working `gitea:gitea-prs` is the user-scope copy answering — not evidence that the apm install or this file is broken, and not something to "fix". Prefer the bare name anyway: apm deploys it, an external consumer installing holocron through apm gets it, and it is the form that survives those user-scope installs eventually being converted. The namespaced form also still resolves 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`.
|
||||
Use the bare, **unnamespaced** names. That is what `apm install` deploys and the only form this repo's own install produces — a project skill has no plugin to prefix (ADR-0018). Whether the `<plugin>:` form (`gitea:gitea-prs`) also resolves depends on native plugin installs at user scope, outside this repo; write the bare name either way.
|
||||
|
||||
Fall back to raw shell only when no skill covers it.
|
||||
|
||||
## Setup and testing
|
||||
## Session rules
|
||||
|
||||
- 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`.
|
||||
- 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 14-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 `python3` — required by `scripts/skill-size-check.sh`, the `skill-size-check` pre-commit hook. It measures the *folded* `description` value: most descriptions here are `>`-block scalars, so a regex over the raw lines measures indentation and newlines instead of the value. Missing it fails the hook with an install pointer rather than skipping the ADR-0020 checks, which would be a vacuous green. In practice it is already present — pre-commit is itself a Python application. PyYAML is used when importable and is genuinely optional; a fallback reader covers the frontmatter shapes this corpus uses.
|
||||
- That hook enforces **two independent gate families** over `plugins/*/.apm/skills/*/SKILL.md`, and neither replaced the other. The agentskills.io spec backstop is unchanged: 500 lines and 2,770 words, counted over the **whole file including frontmatter**. ADR-0020 adds a context budget measured differently — `description` 250 chars SUGGESTION / 400 FAIL (it is preloaded into every session whether the skill fires or not), **body-only** word count 600 SUGGESTION / 900 FAIL (everything after the frontmatter's closing `---`), and every boundary-clause routing target resolving to a real skill or agent under `plugins/*/.apm/`. A file can sit well inside one family and fail the other. The hook is `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. `skill-audit`'s `validate.sh` holds a second copy of the four ADR-0020 constants; `tests/test-skill-size-check.sh` asserts the copies agree.
|
||||
- **Those ADR-0020 gates ship hot, with no baseline file.** 26 of 39 descriptions and 9 of 39 bodies currently exceed their FAIL tier, so editing one of those skills *for any reason* means retrofitting it to the contract first — a one-line fix to `gitea-prs` cannot be committed until that skill complies. This is deliberate, and the retrofit is tracked as Gitea issue #99. Check where a skill stands before starting: `pre-commit run skill-size-check --all-files`.
|
||||
- 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.
|
||||
- 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.
|
||||
- `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 14 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`, `check-executables-allow-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`. `check-executables-allow-sync` is the odd one in that first group — it guards a silent failure rather than drift in generated text. apm gates a package's `hooks/` and `bin/` on an exact `<package>#<version>` lookup in root `apm.yml`'s `executables.allow`, with no wildcard and no version-less form, so bumping `plugins/kyberforge/apm.yml`'s `version:` without bumping the key errors nowhere: the entry simply stops matching, kyberforge's `SessionStart` hook stops deploying, and the install goes quietly stale — the failure ADR-0019 records as live. Run `pre-commit run --hook-stage pre-push --all-files` locally — one command, the whole gate. That command reports **16**, not 14: 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.
|
||||
- `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. Agents take the ADR-0020 description gates (`agent-audit`'s `validate.sh` holds its own copy of those two constants) and, deliberately, **no** body word gate: an agent body becomes the system prompt of a fresh context rather than competing with the caller's live conversation, so the 900-word FAIL does not transfer. A bats test pins that absence — adding a body gate there contradicts the ADR rather than fixing an inconsistency.
|
||||
- **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 twelve pre-push hooks pass offline because they are real local checks (`check-executables-allow-sync` landed after that run, but reads two local manifests and makes no network call), 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-commits` — it validates Conventional Commits (enforced at `commit-msg`) for you.
|
||||
- **Do not add repo-owned keys to `.claude/settings.json`.** apm treats it as its own deployed artifact and `apm audit --ci` replays the install and diffs, so anything apm would not have written is permanent drift that fails the `apm-audit-ci` pre-push hook. A hook you want here is authored in `plugins/<name>/.apm/hooks/` and deployed by apm, never hand-written into that file. The `SessionStart` entry already in it is exactly that: kyberforge authors it in `plugins/kyberforge/.apm/hooks/hooks.json` and apm merges it in, so it is apm's own output, it is what the replay expects, and it belongs in the commit — do not strip it (ADR-0019). Machine-specific settings go in the gitignored `.claude/settings.local.json`; shared enforcement goes in `.pre-commit-config.yaml`.
|
||||
- **`apm.lock.yaml` turning up modified is expected, not a bug.** kyberforge's `SessionStart` hook runs `apm outdated` at startup and `apm update --yes` when something is behind, which rewrites the lock. Commit or discard it deliberately.
|
||||
- **A `.apm/` edit is not live in this session until it is pushed.** The six dependencies resolve from the holocron remote, unpinned against the default branch. `apm install` deploys from the lock; `apm update` is what re-resolves refs.
|
||||
- **The ADR-0020 skill gates ship hot, with no baseline.** 26 of 39 descriptions and 9 of 39 bodies exceed their FAIL tier, and the `Kyberforge.CompositionNote` Vale rule fires 10 errors across `gitea-issues`, `gitea-labels-milestones`, `gitea-prs` and `gitea-workflow`. Editing any of those skills *for any reason* means retrofitting it to the contract first — a one-line fix cannot be committed until the skill complies. Deliberate; tracked as Gitea issue #99. `skill-size-check` will not warn you about the Vale half, so check both: `pre-commit run --all-files`.
|
||||
- **Run `bash tests/run-tests.sh --strict` before considering any change done.** Keep the flag: without it a suite whose dependency is missing exits 77 and is counted SKIPPED rather than failed, so the run goes green having verified less than it claims.
|
||||
- **Before pushing, rehearse the gate locally:** `pre-commit run --hook-stage pre-push --all-files`. It runs the 14 pre-push hooks this repo authors itself plus pre-commit's 2 `meta` hooks, so it prints 16; `check-release-needed` passes without checking anything, because it needs a real push to `main`. `docs/spec/gates.md` reconciles both.
|
||||
- **Pushing without a network** needs `SKIP=apm-marketplace-check,apm-pack-check-clean git push` — those two resolve a remote marketplace entry via `git ls-remote`. Skip only those two; the rest are real local checks, and adding one to `SKIP` disarms it silently.
|
||||
- **Author commits with `git-commits`** — it validates Conventional Commits, which `commit-msg` enforces.
|
||||
- **This repo and Gitea are the only source of truth.** All project state, decisions, and working conventions live here. Do not use an external memory system for this project — cached state diverges from the repo and you get a split brain. Before answering any design or architecture question, check `docs/adr/` for an existing decision.
|
||||
|
||||
## Key documents
|
||||
|
||||
Read CONTEXT.md at the start of every session in this repo.
|
||||
Read `CONTEXT.md` at the start of every session — it is this repo's domain glossary, and the terms it defines are used unglossed everywhere else. It is not exhaustive: terms it does not carry are defined at their point of use, mostly in `docs/spec/`.
|
||||
|
||||
Read these on demand:
|
||||
|
||||
- `docs/spec/architecture.md` — current directory structure, install pipeline, provider model
|
||||
- `README.md` — prerequisites, install, and test commands
|
||||
- `docs/VISION.md` — the phased roadmap and where this is going; read when a decision turns on product direction
|
||||
- `LESSONS.md` — patterns that went wrong once; read before repeating a class of change that has burned the repo before
|
||||
- `docs/spec/gates.md` — what each pre-commit and pre-push hook enforces and why; read when a gate fails or before changing hook config
|
||||
- `docs/spec/architecture.md` — directory structure, install pipeline, provider model
|
||||
- `docs/adr/` — architectural decisions; read before answering design questions or proposing structural changes
|
||||
- `docs/ai-constitution.md` — full governance evidence base; read when a governance decision needs justification
|
||||
- `docs/research/ai-coding-factory/ai-coding-factory-principles.md` — factory design rationale; read when implementing, auditing, or reviewing skills or factory structure
|
||||
|
||||
+188
-68
@@ -1,106 +1,226 @@
|
||||
---
|
||||
name: AI Development Repo
|
||||
description: Domain language and decisions for the global AI development config repository
|
||||
description: The domain language of the global AI development config repository
|
||||
---
|
||||
|
||||
# Context
|
||||
# AI Development Repo
|
||||
|
||||
## Principles
|
||||
The bounded context of this repo is **how agent instructions are authored, packaged, distributed, and
|
||||
kept small**. Terms here name concepts specific to that problem. Mechanics live elsewhere:
|
||||
`docs/spec/architecture.md` for structure, `docs/spec/gates.md` for enforcement, `docs/adr/` for
|
||||
decisions.
|
||||
|
||||
### CLAUDE.md index model
|
||||
`AGENTS.md` is the source of always-on universal rules (provider-agnostic). `providers/claude-code/CLAUDE.md` is a thin adapter: it imports `~/.agents/AGENTS.md` via `@~/.agents/AGENTS.md` and appends Claude Code-specific additions (`@import` for governance.md, content index). Deployed to `~/.claude/CLAUDE.md` via `install.sh`. Context size is kept minimal — only what is needed every session is loaded upfront; detailed content is pulled on demand. See ADR-0003.
|
||||
## Language
|
||||
|
||||
### Instruction file format
|
||||
`core/instructions/<topic>.md` files are plain markdown — no frontmatter, no schema. The agent decides when to read each file based on task context and the content index label in `providers/claude-code/CLAUDE.md`. Frontmatter is deferred until there is evidence that agents are loading the wrong files in practice.
|
||||
### Context cost
|
||||
|
||||
### Repo/gitea as source of truth
|
||||
All project state, decisions, context, and working conventions live in this repo or Gitea. External memory systems should not be used for this project — they create a split-brain risk where cached state diverges from the repo. At the start of every session, read `CLAUDE.md`, `CONTEXT.md`, and `docs/VISION.md`. Everything needed to orient is here.
|
||||
**Preload tax**:
|
||||
The always-on context cost of every installed skill's `name` and `description`, charged from the
|
||||
first token of every session whether the skill is invoked or not. Measurement method and current
|
||||
figure: ADR-0020.
|
||||
_Avoid_: context cost, token overhead
|
||||
|
||||
Before answering any design or architecture question, check for existing decisions: `docs/adr/` (hard architectural decisions).
|
||||
**Skill context contract**:
|
||||
The ADR-0020 authoring rules that hold the preload tax and body size down — a description carries a
|
||||
trigger clause, at most one capability clause, and a boundary clause, and nothing else. Thresholds
|
||||
and the target-resolution walk: `docs/spec/gates.md`.
|
||||
_Avoid_: skill budget, size limit
|
||||
|
||||
## Glossary
|
||||
**Dispatch body**:
|
||||
The body pattern a skill with two or more mutually exclusive flows must use — the body carries only
|
||||
the dispatch table and the gates common to every branch, and each flow lives in its own
|
||||
self-contained `references/` file. Exemplar: `apm-workflow`.
|
||||
_Avoid_: router body, thin body
|
||||
|
||||
### Management Application
|
||||
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.
|
||||
**Hand-invoked skill**:
|
||||
A skill reached only by typing its slash command, declared `disable-model-invocation: true`. The host
|
||||
withholds it from the model-visible listing entirely, so it pays no preload tax and its description
|
||||
becomes human-facing text. Exemplar: `zoom-out`.
|
||||
_Avoid_: manual skill, disabled skill
|
||||
|
||||
### Skills
|
||||
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`).
|
||||
**Delegation discipline**:
|
||||
The agent-side counterpart to the dispatch body. A plugin-scope agent is a single `.agent.md` file
|
||||
with no sibling `references/` directory, so it cannot disclose to itself — it can only delegate to
|
||||
skills. Its characteristic defect is therefore restatement, not length.
|
||||
_Avoid_: agent hygiene
|
||||
|
||||
### Preload tax
|
||||
The always-on context cost of every installed skill's `name` + `description`, which sit in the agent's context from the first token of every session whether or not the skill is invoked. Measured 2026-08-14 at 23,612 chars (~6,200 tokens) across 39 skills, plus 1,325 chars for 4 agents. Non-routing frontmatter (`metadata.source_keys`, `category`, `version`) is **not** part of it — the model-visible skill listing carries only `name` and `description`, which supersedes `LESSONS.md:63` on this host. Bodies are not part of it either; they are charged on invocation.
|
||||
### Distribution
|
||||
|
||||
### Skill context contract
|
||||
The authoring rules that hold the preload tax and body size down, set by ADR-0020. A description carries a trigger clause, at most one capability clause, and a boundary clause of the form `Not <thing> → <skill-name>` naming a resolvable target — nothing else. Capability enumeration, output formats, and composition notes ("composes X rather than duplicating Y") belong in the body or `README.md`; a description that summarises workflow is a correctness hazard, not just a cost, because agents act on it instead of reading the body. Sizes are two-tier and sit *below* the agentskills.io spec limits, which stay unchanged as conformance backstops: description 250 SUGGESTION / 400 FAIL (spec 1,024); body 600 SUGGESTION / 900 FAIL (spec 2,770 words / 500 lines). Conflating the quality gate with the spec ceiling is what let `skill-author` and `agent-author` grow to within twelve words of 2,770.
|
||||
**Skill**:
|
||||
A reusable slash command defined as a `SKILL.md` file following the
|
||||
[Agent Skills open standard](https://agentskills.io), authored at
|
||||
`plugins/<plugin>/.apm/skills/<skill>/SKILL.md`.
|
||||
_Avoid_: command, prompt, macro
|
||||
|
||||
### Dispatch body
|
||||
The body pattern a skill with two or more mutually exclusive flows must use: the body carries only the dispatch table and the gates common to every branch, and each flow lives in its own self-contained `references/` file. Named for `apm-workflow` (554-word body, 3,006 words of references), which arrived at it independently and is the repo's exemplar. Its absence is the characteristic defect — `skill-author` inlines both its create and improve flows, and `agent-author` carries 50-60 lines marked inapplicable by their own headers on any single run.
|
||||
**Plugin**:
|
||||
The deployable unit — one or more skills, agents, hooks, commands, and MCP servers bundled into a
|
||||
single installable directory under `plugins/<name>/`, compiled from that plugin's `.apm/` source.
|
||||
_Avoid_: package, bundle, module
|
||||
|
||||
### Hand-invoked skill
|
||||
A skill reached only by typing its slash command, declared with `disable-model-invocation: true`. The host withholds it from the model-visible skill listing entirely, so it pays no preload tax and its `description` becomes human-facing text rather than a trigger list. `zoom-out` is the worked example: apm passes the flag through verbatim to both install paths, and the skill is absent from the router while `/zoom-out` still works. Choosing model-invoked vs. hand-invoked is the first question `skill-author` asks, because it determines whether a description needs triggers at all.
|
||||
**apm package**:
|
||||
The unit apm builds and installs — `plugins/<name>/apm.yml` plus the hand-authored
|
||||
`plugins/<name>/.apm/` tree it compiles from (ADR-0015).
|
||||
_Avoid_: plugin directory, source tree
|
||||
|
||||
### Delegation discipline
|
||||
The agent-side counterpart to the dispatch body. A plugin-scope agent is a single `.apm/agents/<name>.agent.md` file with no sibling `references/` directory, so it cannot disclose to itself — it can only delegate to skills. Its characteristic defect is therefore restatement, not length: an agent body that spells out a procedure a skill it can invoke already owns creates a second copy that drifts. `agent-audit` fails that, with the fix being "invoke `<skill>` instead". Agents take the same description gates as skills but no body word gate — a skill body competes with the caller's live conversation, an agent body becomes the system prompt of a fresh context.
|
||||
**Content mirror**:
|
||||
The generated flat `skills/`, `agents/`, `commands/`, `instructions/`, `extensions/` directories and
|
||||
merged `hooks/hooks.json` at a plugin root — also called the flat mirror — compiled from that
|
||||
plugin's `.apm/` tree so hosts that convention-scan those paths discover the content (ADR-0017).
|
||||
_Avoid_: generated copy, duplicate tree
|
||||
|
||||
### 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>`, or consume it as an apm dependency (see "apm-consumed install").
|
||||
**Output profile**:
|
||||
An `apm pack` target format for a generated *marketplace* manifest; apm has `claude`
|
||||
(`.claude-plugin/marketplace.json`) and `codex` (the differently-shaped
|
||||
`.agents/plugins/marketplace.json`), and none for `.github/plugin/marketplace.json` (Copilot CLI's
|
||||
legacy path), which a sync script mirrors instead. Mechanics: `docs/spec/architecture.md`.
|
||||
_Avoid_: build target, export format
|
||||
|
||||
### 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.
|
||||
**Plugin marketplace**:
|
||||
A Git repository carrying a `marketplace.json` manifest that lists installable plugins. There is no
|
||||
backend, registry, or SaaS — the Git repo is the marketplace.
|
||||
_Avoid_: registry, store, catalogue
|
||||
|
||||
### 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 — apm installs nothing 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.
|
||||
**holocron**:
|
||||
This repository, in its role as a plugin marketplace and as the remote the six plugin dependencies
|
||||
resolve against.
|
||||
_Avoid_: the marketplace, upstream
|
||||
|
||||
Four consequences, each load-bearing:
|
||||
- **Skills gain an unnamespaced name.** apm deploys plain project skills, so `git:git-commits` also answers to `git-commits`. The `<plugin>:` form has not stopped resolving here: `~/.claude.json` still enables `core`, `git`, `gitea`, `kyberforge`, and `lint` at user scope, which ADR-0018 left in place deliberately — converting them is a separate decision with a blast radius beyond this repo. Until it is taken, every skill is live under two names, which is the same "present twice under two names" outcome ADR-0018's own "Alternatives considered" rejected for *keeping both install paths* — reached here by leaving user scope alone rather than by adopting it as the install model. Write the bare name regardless: apm deploys it, and a repo consuming holocron through apm gets only that form. The namespaced form still resolves 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.
|
||||
**apm-consumed install**:
|
||||
How this repo installs its own plugins as of 2026-08-14 — six `dependencies.apm` entries in the root
|
||||
`apm.yml` deployed by `apm install`, rather than `claude plugin install <name>@holocron`. Its
|
||||
consequences: ADR-0018.
|
||||
_Avoid_: apm install, dependency install
|
||||
|
||||
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.
|
||||
**Provenance chain**:
|
||||
The three-stage traceability record linking a skill back to its research inputs: `/research` produces
|
||||
topic docs and a `sources.md`; the author skill records which sources informed which files in
|
||||
`references/sources.md` and `source_keys` frontmatter; `skill-audit` validates the chain is complete
|
||||
and internally consistent.
|
||||
_Avoid_: sources, citations, attribution
|
||||
|
||||
### 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.
|
||||
### Governance
|
||||
|
||||
### HOTL (human-on-the-loop)
|
||||
Agent acts; human monitors and can intervene after the fact. Acceptable for low-stakes, bounded, reversible actions where the cost of pausing for approval exceeds the blast radius of an error. The distinction between HITL and HOTL must be explicit and documented — defaulting to HOTL for convenience is not acceptable.
|
||||
**HITL** (human-in-the-loop):
|
||||
The agent pauses before a consequential action and a human approves before execution. Required for
|
||||
irreversible or high-stakes actions — architecture changes, production deployments, security
|
||||
configuration.
|
||||
_Avoid_: manual approval, gated action
|
||||
|
||||
### Sycophancy
|
||||
The failure mode where RLHF-trained models prioritise approval over accuracy. Treated as a first-class reliability risk: models change correct answers to wrong ones under user pressure in a majority of observed cases, then persist in the wrong answer. Designing against sycophancy is an explicit obligation, not a quality-of-life concern. Countermeasures: explicit pushback resistance instructions, prompting for dissent, cross-validating against independent sources. Never interpret AI agreement as AI accuracy.
|
||||
**HOTL** (human-on-the-loop):
|
||||
The agent acts and a human monitors, able to intervene after the fact. Acceptable only for
|
||||
low-stakes, bounded, reversible actions where the cost of pausing exceeds the blast radius of an
|
||||
error.
|
||||
_Avoid_: autonomous, unsupervised
|
||||
|
||||
### AGENTS.md
|
||||
The provider-agnostic always-on instruction entry point. Two files:
|
||||
- **Repo-level `AGENTS.md`** — instructions for agents working inside this repo (structure, key rules); imported by repo `CLAUDE.md` via `@AGENTS.md`.
|
||||
- **Global `core/AGENTS.md`** — Communication and Behavior rules that apply across all projects; deployed to `~/.agents/AGENTS.md`; imported by `~/.claude/CLAUDE.md` via `@~/.agents/AGENTS.md`.
|
||||
**Sycophancy**:
|
||||
The failure mode where an RLHF-trained model prioritises approval over accuracy — changing a correct
|
||||
answer to a wrong one under user pressure, then persisting in the wrong answer. Treated here as a
|
||||
first-class reliability risk, not a quality-of-life concern.
|
||||
_Avoid_: agreeableness, people-pleasing
|
||||
|
||||
Contains always-on rules in plain markdown with no provider-specific syntax (no `@import`). Provider-specific files (`CLAUDE.md`) are thin adapters that import the relevant `AGENTS.md` and add only Claude Code-specific syntax. This pattern means a single source of truth can serve multiple providers without duplication. See ADR-0003.
|
||||
### Documents
|
||||
|
||||
### Skill composition
|
||||
A skill calling another skill by name to delegate a sub-task. The calling skill focuses on the orchestration decision ("when to do X"); the called skill owns the mechanics ("how to do X"). Established compositions: `grill-me` calls `write-adr` when a decision crystallises; `implement-feature` calls `tdd` as its implementation methodology; `forge` calls `grill-with-docs` to refine intent, classifies the target artifact type (skill / agent / plugin / marketplace entry), then routes to the matching `*-author` skill — which owns its own create/improve logic and, where applicable, its own inline audit closeout (`skill-author` runs `/skill-audit`, `agent-author` runs `kyberforge:agent-audit`, both in the same context as the authoring work). Reserve `forge` for genuinely undecided "which artifact type is this" questions — an already-fully-specified corrective edit (exact file, line, and fix already known) should call the target author skill directly instead (`skill-author`, `apm-workflow`, `agentsmd-author`, etc.); routing a known fix through `forge`'s grill-and-classify layer adds unnecessary indirection and, in practice, has been observed to lose track of hard constraints handed down the chain (e.g. "don't commit yet," "edit in this worktree") because each hop re-derives instructions from a shorter brief. `forge` additionally runs its own independent recheck after a skill/agent route finishes: a clean-context subagent (not forked, no inherited context) re-runs the same audit skill against the finished artifact, as a distinct verification layer from the author skill's inline audit — the two can share blind spots since the inline audit runs in the same context as the work it checks. If the clean audit surfaces any unresolved finding, `forge` loops — re-invoke the author skill to resolve it, re-run the clean audit — until the clean audit comes back with nothing unresolved; only then is the route done. `plugin-author` and `marketplace-author` had no audit counterpart and got no recheck; their terminal check was `claude plugin validate`. Both were deprecated per ADR-0015, superseded by `apm-workflow`, and deleted entirely once issue #90 landed.
|
||||
**AGENTS.md**:
|
||||
The provider-agnostic always-on instruction file, in plain markdown with no provider-specific syntax
|
||||
(ADR-0003). Two exist: repo-level, and the global `core/AGENTS.md` deployed to `~/.agents/AGENTS.md`.
|
||||
_Avoid_: instructions file, system prompt
|
||||
|
||||
### Provider-agnostic issue tracker
|
||||
Skills and workflows reference "linked issue" generically rather than a specific provider. Gitea is the canonical issue tracker for this repo (see ADR-0007). "Issue" is the cross-provider term (GitHub, GitLab, Gitea all use it).
|
||||
**Thin adapter**:
|
||||
A provider-specific instruction file (`CLAUDE.md`, `.cursor/rules/*.mdc`, `copilot-instructions.md`)
|
||||
that imports its `AGENTS.md` and adds only that provider's syntax, carrying no original always-on
|
||||
content of its own (ADR-0002, ADR-0003).
|
||||
_Avoid_: wrapper, shim, provider file
|
||||
|
||||
### Provenance chain
|
||||
The three-stage traceability record linking a skill back to its research inputs: (1) `/research` produces topic docs and a `sources.md` in `plugins/<plugin>/docs/research/docs/<topic>/`; (2) `/skill-author` reads those docs and records which sources informed which skill files in `references/sources.md` (including a `Research doc:` back-pointer to the upstream research file) and `source_keys` frontmatter on `SKILL.md` and `references/*.md`; (3) `skill-audit` validates the chain is complete and internally consistent via `validate-provenance.sh`. A skill with research input but no `references/sources.md`, or with `source_keys` that don't match `references/sources.md` slugs, has a broken provenance chain.
|
||||
**LESSONS.md**:
|
||||
The long-loop feedback log for patterns observed across sessions, at the repo root.
|
||||
_Avoid_: changelog, retro, postmortem
|
||||
|
||||
### Bidirectional reference principle
|
||||
Files that reference other files should declare those references explicitly. The referencing file carries the forward reference (e.g. content index in `CLAUDE.md`, `references:` in frontmatter). The referenced file carries a `when:` field describing when it is loaded. Both sides should agree — divergence signals staleness. The reverse map ("what files reference this file?") is derived by a reference scanner script, not maintained manually. This principle applies to instruction files, skills, and workflow documents.
|
||||
**Management Application**:
|
||||
A separate product in a separate repo for browsing, editing, and configuring AI development configs
|
||||
through a product UI, with Git as an invisible persistence layer. Repo-agnostic; this repo is its
|
||||
canonical default content. Roadmap: `docs/VISION.md`.
|
||||
_Avoid_: the UI, the dashboard, the app
|
||||
|
||||
### agentsmd-author / agentsmd-audit
|
||||
A skill pair in the `core` plugin for writing, updating, and reviewing a repo's `AGENTS.md` file(s) — the generic open-standard file (see the `AGENTS.md` entry above), including this repo's own. `agentsmd-author` creates/updates AGENTS.md content, supports nested monorepo placement (per the standard's nearest-file-wins precedence), and closes out by invoking `agentsmd-audit` inline. `agentsmd-audit` runs a single combined pass checking three mandatory baselines: secrets/credentials (governance.md hard prohibition — AGENTS.md is committed content), structural completeness (common-sections checklist from the agents.md spec), and accuracy/drift (do referenced commands and paths actually resolve against the repo). `agentsmd-audit` never inspects provider adapter files (see `provider-adapter-author`) — its scope is AGENTS.md content only. Chosen over folding this into `kyberforge` because kyberforge's scope is meta-tooling for the holocron marketplace itself, not generic target-repo documentation; `core` is the intended home for cross-cutting, repo-agnostic utility skills.
|
||||
### Quality
|
||||
|
||||
### provider-adapter-author
|
||||
A companion skill (`core` plugin) that detects a target repo's provider-specific instruction file (`CLAUDE.md`, `.cursor/rules/*.mdc`, `copilot-instructions.md`, etc.) and, where it duplicates content AGENTS.md should own, converts it into a thin adapter that imports AGENTS.md — mirroring this repo's own ADR-0002/ADR-0003 two-tier adapter pattern. Self-validates via its own bundled deterministic script (`scripts/validate-adapter.sh`: checks for an import reference, no duplicated headings, size threshold) rather than a separate paired audit skill — the check is mechanical, so a script suffices per governance.md's "prefer deterministic code for repeatable tasks." `agentsmd-author` calls this skill via skill composition when it detects an existing provider file with overlapping content.
|
||||
**Skill composition**:
|
||||
A skill calling another skill by name to delegate a sub-task — the caller owns the orchestration
|
||||
decision ("when to do X"), the callee owns the mechanics ("how to do X").
|
||||
_Avoid_: chaining, nesting, sub-skill
|
||||
|
||||
### lint plugin
|
||||
A standalone, repo-agnostic plugin (`plugins/lint/`) for configuring and running linters — not scoped to kyberforge's own meta-tooling. First linter is Vale (prose style linting), split into two skills per the git/gitea per-concern pattern: `vale-config` (setup — `.vale.ini`, `StylesPath`, styles) and `vale-run` (invoke Vale, interpret/report findings). A `lint-runner` agent composes these for isolated-context lint sweeps; it is report-only **by instruction, not by capability** — its body states "You never edit files" and "Do not edit, fix, or rewrite any flagged content", but nothing enforces that. It previously carried `tools: Bash, Read, Grep, Glob`, which withheld `Edit` outright; plugin-scope APM agents cannot express a `tools:` field at all (ADR-0016 — `apm compile` copies frontmatter verbatim to both Claude Code and Copilot, whose `tools:` vocabularies are incompatible, so a value correct for one harness is wrong for the other), so `plugins/lint/.apm/agents/lint-runner.agent.md` now declares only `name`/`description`/`source_keys` and inherits every tool, `Edit` included. ADR-0016 accepted this loss of enforcement knowingly; the restriction survives as prose the agent is expected to follow. Vale's research docs (`docs/research/docs/vale/`) moved from `plugins/kyberforge/` to `plugins/lint/` to keep the provenance chain same-plugin.
|
||||
**Vale audit prefilter**:
|
||||
The deterministic Vale pass that runs ahead of `skill-audit`/`agent-audit`'s Description dimension,
|
||||
so LLM judgment is spent only on what a pattern cannot catch. Mechanics: `docs/spec/gates.md`.
|
||||
_Avoid_: linting, style check
|
||||
|
||||
### Vale audit prefilter (skill-audit / agent-audit)
|
||||
Wiring Vale as a deterministic prefilter for `skill-audit`/`agent-audit`'s Description dimension (ADR motivation: issue #84) is repo-specific, not part of the generic `lint` plugin, so it doesn't live in `plugins/lint/` — but per ADR-0014 it also doesn't live at the repo root anymore. Two copies live inside `plugins/kyberforge/`, one per skill, since a plugin's cache-install only copies each skill's own files (no cross-skill sharing): `plugins/kyberforge/.apm/skills/agent-audit/assets/vale/` is canonical (`.vale.ini` plus a custom `Kyberforge` style covering description-opener banning ("This skill/agent..."), vague-capability wording ("helps with", "utilize", ...), and generic "see references/ for details" padding — and a `KyberforgeCopilot` style scoped only to `.agent.md` files for the Copilot-only "Use proactively has no effect" check), and `plugins/kyberforge/.apm/skills/skill-audit/assets/vale/` is a smaller duplicate (`Kyberforge` only, scoped to `SKILL.md`) kept in sync by `scripts/check-vale-style-sync.sh` (pre-push). A root-level `.pre-commit-hooks.yaml` exposes both copies (plus `skill-size-check`) so any external repo can enforce the same rules via `repo: <this-repo-url>, rev: <tag>` in its own `.pre-commit-config.yaml` — pre-commit clones the pinned rev into its own cache, independent of whether Claude Code or the `kyberforge` plugin is installed at all, and the same mechanism covers CI (`pre-commit run --all-files`). This repo's own `vale-audit-prefilter-skill`/`-agent` pre-commit hooks consume the identical plugin-bundled copies via `repo: local` (not a third root copy, and not a pinned self-reference — a pinned self-reference would lint working-tree edits against the last tagged release rather than the change being made). Every rule is `level: error` and every alert is a FAIL — no ignorable tier, same as shellcheck, the test suite, and conventional-pre-commit. Graded severities do not work here: Vale's exit code keys on `error` alerts alone, so `warning`/`suggestion` rules exit 0 and pre-commit swallows the output of a passing hook, leaving them invisible and blocking nothing. `MinAlertLevel` and `--minAlertLevel` are correspondingly absent from `.vale.ini` and the hook, being no-ops under this model. Vale covers the pattern-matchable sub-checks named in issue #84 (imperative opener, vague filler, `Use proactively`, generic reference-pointer padding) plus, per ADR-0013, one body-wide prose-pattern check ("There is/are" sentence openers) — everything else about body discipline (defaults-vs-menus, why-rationale, non-pattern-matchable judgment calls), near-miss exclusion strength, and control calibration stays LLM judgment.
|
||||
**Authoring root**:
|
||||
The directory a gate resolves against — the nearest ancestor of the file being checked holding
|
||||
`plugins/*/.apm/skills` or `plugins/*/.apm/agents`, falling back to the nearest ancestor holding
|
||||
`.git`. The walk: `docs/spec/gates.md`.
|
||||
_Avoid_: repo root, project root
|
||||
|
||||
Both skills' Step 1, and the `vale-audit-prefilter-skill`/`-agent` pre-commit hooks, call each copy's own `scripts/vale-wrap.sh` rather than `vale` directly — a workaround for a confirmed Vale 3.15.2 limitation (see `vale-config`'s Gotchas): `text.frontmatter.description` silently stops matching on most — not all — multi-line descriptions. Verified by reproduction, not assumed: `>` folded scalars, plain (unquoted) continuation lines, and single- or double-quoted multi-line scalars all yield 0 alerts and exit 0 on a deliberately-bad fixture, while a `|` literal block spanning the same 2+ lines lints normally (alerts fire, exit 1). The wrapper flattens those three broken forms to one physical line in a scratch copy (padding with blank lines so every other line number is unchanged) before handing off to real `vale`; `|` literal blocks and single-line descriptions pass through untouched, already linting correctly. The plain and quoted forms previously passed silently — unflattened and unmatched — so a bad description in either sailed through the prefilter. Handed no `--config` at all, the wrapper falls back to its own sibling `assets/vale/.vale.ini`, located from `${BASH_SOURCE[0]}` rather than from the cwd — which is why both manifests' `entry:` is now the bare script path with no argument after it. pre-commit prefixes only `entry[0]` with the hook-repo clone path (`cmd = (prefix.path(cmd[0]), *cmd[1:])`), so every later argument resolves against the *consuming* repo's root: a `--config` in `.pre-commit-hooks.yaml` pointed at a path no consumer has and hard-failed every external run with `E100 [--config] Runtime error`. `.pre-commit-config.yaml` drops the argument too, deliberately keeping the two entries identical — the local `repo: local` hook resolved its `--config` correctly only because the consuming repo *was* this repo, and that divergence is why three review rounds exercised a path no external consumer takes and missed the defect. An explicit `--config` still wins, in all three argv forms (`--config X`, `--config=/abs`, `--config=rel`), and a relative one still resolves against the caller's cwd, matching bare `vale`, not the repo root. Both audit skills' Step 1 now passes no `--config` either: it resolves the script relative to the skill's own directory so the call works from an installed plugin cache, but a relative `--config` alongside it would still resolve against the cwd, yielding `E100 Runtime error ... does not exist` and exit 2 — which both skills' fallback misreads as "vale unavailable" and silently downgrades to full LLM judgment. `tests/test-vale-wrap.sh` regression-tests this against skill-audit's copy specifically (its fixtures are all `SKILL.md`-shaped, and only skill-audit's `.vale.ini` has that glob section). Each `.vale.ini`'s section globs are path-agnostic (`[**/SKILL.md]` for skill-audit's copy; `[**/agents/*.md]`/`[**/*.agent.md]` for agent-audit's) and do no scoping on their own: Vale's `*` crosses `/`. Scoping comes from each pre-commit hook's own `files:` regex and from the audit skills passing one explicit file per invocation. The two manifests scope differently on purpose: this repo's `.pre-commit-config.yaml` pins its own layout — `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$` for `-skill`, `^plugins/[^/]+/\.apm/agents/[^/]+\.agent\.md$` for `-agent` — while the shipped `.pre-commit-hooks.yaml` stays layout-agnostic for external consumers whose skills live anywhere, using `(^|/)SKILL\.md$` and `(^|/)agents/[^/]+\.md$|\.agent\.md$`. Both manifests split the prefilter into two hooks precisely because one combined hook pointed at only one copy would silently 0-file-skip the other file type. A `SKILL.md` outside `plugins/` (e.g. project-scope `.claude/skills/foo/SKILL.md`) still matches `[**/SKILL.md]` and gets linted normally — the globs constrain filename shape, not location. Vale reports 0 files only when the path it is handed matches no glob section at all: a differently-named file, or a directory argument holding nothing that matches. That run prints `✔ 0 errors ... in 0 files.` and exits 0, indistinguishable from a clean pass, so both audits treat a 0-file Vale run as NOT RUN and fall back to full LLM judgment.
|
||||
**Near-miss**:
|
||||
A query that shares keywords with this skill but needs a different one — and, by extension, the
|
||||
sibling that would wrongly answer it; boundary clauses exist to exclude genuine near-misses rather
|
||||
than to enumerate siblings. Detail: `skill-audit/references/description-quality.md`.
|
||||
_Avoid_: overlap, similar skill
|
||||
|
||||
This scope expands per ADR-0013: one cherry-picked low-noise `write-good`/`alex` rule landed in `styles/Kyberforge`, `Kyberforge.SentenceOpenerThereIs` (22 held-out hits, both in-corpus hits clean rewrites, zero suppressions). A second, `Kyberforge.VagueQualifier`, was cherry-picked and then deleted: 2 hits across the 41 skill/agent files, one marginal and one an unfixable false positive (`caveman/SKILL.md` quotes `of course` as an example of filler — a mention, not a use) that forced the repo's only Vale suppression comments. Also new is a sibling pre-commit hook, `skill-size-check` (`scripts/skill-size-check.sh`), enforcing agentskills.io's `SKILL.md` ceiling as two blocking gates: `MAX_LINES=500` and `MAX_WORDS=2770` (a word-count proxy for the 5,000-token limit, calibrated to the densest prose measured in this repo — 1.81 tokens per word — so even a worst-case `SKILL.md` at the ceiling stays under 5,000 tokens). Both are inclusive, and `skill-audit/scripts/validate.sh` checks the same pair on the same terms, so a `SKILL.md` can no longer pass its own audit yet be blocked by the commit hook. Scoped to `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$` only, same as `vale-audit-prefilter-skill`, so it never lints `docs/research/examples/` reference skills. It's also exposed in the root-level `.pre-commit-hooks.yaml` as `kyberforge-skill-size-check` — it has no external asset dependency, so it needed no relocation, only exposure to external consumers. File scope (`SKILL.md` + agent files) and enforcement model (rules land directly in `styles/Kyberforge`, blocking immediately, no trial tier) stay unchanged; governance.md/CONTROLS.md were evaluated and excluded as rule sources (nothing prose-pattern-matchable to mine). House convention: banned phrasing that must be mentioned rather than used goes in backticks or a fenced code block — Vale skips code spans and fences, so no suppression is needed; inline `<!-- vale Rule = NO -->` (HTML-comment form; the MDX `{/* */}` form does not work in plain Markdown) is the fallback only where backticking is impossible.
|
||||
**Vacuous green**:
|
||||
A check that reports success because it measured nothing — zero files scanned, an unparsed value read
|
||||
as empty, a conditional branch that never armed.
|
||||
_Avoid_: false pass, clean run
|
||||
|
||||
### LESSONS.md
|
||||
Long-loop feedback log for patterns observed across sessions. Three or more entries on the same pattern graduate to the relevant standing file (e.g. a coding convention, a governance rule). Updated by the session-handoff skill or directly by the human. Lives at the repo root.
|
||||
**Issue**:
|
||||
The cross-provider term for a tracked unit of work. Gitea is this repo's canonical tracker
|
||||
(ADR-0007), but skills say "linked issue" generically rather than naming a provider.
|
||||
_Avoid_: ticket, card, task
|
||||
|
||||
## Relationships
|
||||
|
||||
- A **Plugin** bundles one or more **Skills** and agents; a **Plugin marketplace** lists **Plugins**;
|
||||
**holocron** is this repo wearing that hat.
|
||||
- Every model-invocable **Skill** pays the **Preload tax**. A **Hand-invoked skill** does not — which
|
||||
is the first question to settle when authoring one.
|
||||
- The **Skill context contract** bounds both the **Preload tax** (description) and the body.
|
||||
A **Dispatch body** is how a skill stays inside it; **Delegation discipline** is how an agent does.
|
||||
- **AGENTS.md** is the source of always-on rules; a **Thin adapter** imports it and originates
|
||||
nothing.
|
||||
- **Skill composition** is the caller/callee split. `forge` routes a genuinely *undecided* artifact
|
||||
type to the matching author skill — an already-specified fix (file, line, and change known) calls
|
||||
that author skill directly, because each routing hop re-derives instructions from a shorter brief
|
||||
and has been observed to drop hard constraints handed down the chain.
|
||||
- **HITL** and **HOTL** are exclusive per action class, and the choice must be explicit and
|
||||
documented. **Sycophancy** is why HOTL is not the safe default.
|
||||
- A **Skill** built on research carries a **Provenance chain**; `skill-audit` fails it when broken.
|
||||
- **LESSONS.md** feeds the standing files: three or more entries on one pattern graduate the pattern
|
||||
into the relevant standing document.
|
||||
|
||||
## Example dialogue
|
||||
|
||||
> **Dev:** "This one only fires when someone types the slash command. Does its description still need
|
||||
> trigger words?"
|
||||
> **Maintainer:** "No — that's a **hand-invoked skill**. The host withholds it from the model-visible
|
||||
> listing, so it pays no **preload tax** at all and the description is human-facing text."
|
||||
> **Dev:** "Then the body can be as long as it needs to be?"
|
||||
> **Maintainer:** "Different budget. The **skill context contract** gates the body whether or not the
|
||||
> skill is model-invoked — the description competes with every other skill's description, the body
|
||||
> competes with the caller's live conversation. Four mutually exclusive flows means a **dispatch
|
||||
> body**: table in `SKILL.md`, one `references/` file per flow."
|
||||
> **Dev:** "And if I split it into an agent instead?"
|
||||
> **Maintainer:** "Then you're in **delegation discipline** territory. An agent has no `references/`
|
||||
> to disclose to, so the failure mode flips — it stops being length and starts being restatement of
|
||||
> a procedure some skill already owns."
|
||||
|
||||
## Flagged ambiguities
|
||||
|
||||
- "skill" was used for both the authored `SKILL.md` under `plugins/<name>/.apm/skills/` and the
|
||||
deployed copy under `.claude/skills/` — resolved: the authoring source is the **Skill**; the
|
||||
deployed copy is gitignored `apm install` output and is never edited.
|
||||
- Skills can answer to two names, bare (`gitea-prs`) and namespaced (`gitea:gitea-prs`), depending on
|
||||
whether a native install exists at user scope alongside the apm one (ADR-0018) — resolved: write
|
||||
the bare name, which is the only form `apm install` produces.
|
||||
- "context" means both the model's live token window (the **Preload tax** sense) and the bounded
|
||||
domain this file describes — resolved: unqualified "context" in this repo means the token window.
|
||||
- "audit" was used for both an author skill's inline closeout and `forge`'s independent
|
||||
clean-context recheck — resolved: these are two distinct layers, kept separate precisely because
|
||||
an audit running in the same context as the work it checks shares that work's blind spots.
|
||||
+56
-19
@@ -1,8 +1,8 @@
|
||||
# Lessons
|
||||
|
||||
Patterns observed during development of this repo. Three or more entries on the same pattern → promote to CONTEXT.md (or the relevant instruction file) as a standing rule.
|
||||
Patterns observed during development of this repo. Three or more entries on the same pattern → promote to `docs/spec/architecture.md` (or the relevant instruction file) as a standing rule.
|
||||
|
||||
**Graduation rule:** When three or more entries cover the same pattern, the human reviews and promotes it to the appropriate standing location: `CONTEXT.md` for domain-level principles, `core/instructions/coding.md` for coding conventions, `core/instructions/testing.md` for testing conventions, or `core/instructions/subagent-orchestration.md` for delegation conventions. Those four are the whole set — `core/instructions/` holds `coding.md`, `governance.md`, `subagent-orchestration.md` and `testing.md`, and nothing else. Git conventions have no standing file of their own: promote them to `core/instructions/coding.md`, or create a new instruction file deliberately rather than assuming one exists. The graduated entries are marked `[graduated → target file]` rather than deleted (audit trail).
|
||||
**Graduation rule:** When three or more entries cover the same pattern, the human reviews and promotes it to the appropriate standing location: `docs/spec/architecture.md` for structural and domain-level principles — `CONTEXT.md` is not a destination, its `## Principles` section was deleted and what was there now sits under that file's "AGENTS.md pattern" and "Reference conventions" headings — `core/instructions/coding.md` for coding conventions, `core/instructions/testing.md` for testing conventions, or `core/instructions/subagent-orchestration.md` for delegation conventions. Those four are the whole set — `core/instructions/` holds `coding.md`, `governance.md`, `subagent-orchestration.md` and `testing.md`, and nothing else. Git conventions have no standing file of their own: promote them to `core/instructions/coding.md`, or create a new instruction file deliberately rather than assuming one exists. The graduated entries are marked `[graduated → target file]` rather than deleted (audit trail).
|
||||
|
||||
**Who writes here:** The session-handoff skill (Chunk 3) prompts LESSONS.md extraction before closing a session. The human may also write directly.
|
||||
|
||||
@@ -26,7 +26,7 @@ Issue files frequently referenced "the workflow defined in `docs/notes/skill-imp
|
||||
|
||||
The repo CLAUDE.md instructs agents to read CONTEXT.md at session start, but agents skip this in practice — defaulting to reading only what's directly relevant to the immediate prompt (e.g. the skills folder). The governance.md works because `@import` is technically enforced by Claude Code. Fix: (1) add `@CONTEXT.md` to repo CLAUDE.md using `@import` to make it always-loaded; (2) add a "Key decisions" section to CONTEXT.md with one-line resolved-ADR summaries so locked choices are always in context.
|
||||
|
||||
**Status (2026-08-14): neither part landed.** Root `CLAUDE.md` imports `@AGENTS.md` only — no `@CONTEXT.md` — and `CONTEXT.md` has no "Key decisions" section. The behavioral hope this entry diagnosed is still the only mechanism in place: `AGENTS.md` carries the line "Read CONTEXT.md at the start of every session in this repo," which is loaded but is itself an instruction, not an import. The proposal above is open work, not a record of a completed change.
|
||||
**Status (2026-08-14): neither part landed.** Root `CLAUDE.md` imports `@AGENTS.md` only — no `@CONTEXT.md` — and `CONTEXT.md` has no "Key decisions" section. The behavioral hope this entry diagnosed is still the only mechanism in place: `AGENTS.md` carries the line "Read `CONTEXT.md` at the start of every session," which is loaded but is itself an instruction, not an import. The proposal above is open work, not a record of a completed change.
|
||||
|
||||
## 2026-05-17 — Instruction rules lose to RLHF defaults without specificity
|
||||
|
||||
@@ -194,7 +194,11 @@ is the broken multi-`raw:` form, which `tests/test-vale-hooks-consumer.sh` now f
|
||||
## 2026-08-14 — Un-anchoring a description rule to reach mid-sentence text is unshippable
|
||||
|
||||
Widening `DescriptionOpener` to catch `gitea-workflow`'s mid-description "This is the human-facing
|
||||
entry point…" looked like a one-character change. Under `scope: text.frontmatter.description`, `^`
|
||||
entry point…" looked like a one-character change. Both that skill and `gitea-labels-milestones`
|
||||
*open* with "Use when…" and satisfy the opener rule; the offending clause sits at character 377 and
|
||||
300 of the folded value respectively, so the rule was never violated and never silently passed — it
|
||||
simply had no jurisdiction, which is a different defect and takes a different fix.
|
||||
Under `scope: text.frontmatter.description`, `^`
|
||||
anchors to the start of the whole description value — and `vale-wrap.sh` has already flattened that
|
||||
value to one physical line, so `(?m)` changes nothing. Un-anchoring is therefore the only route to
|
||||
mid-description text, and measured across the corpus it scores 5 hits and 5 false positives: skills
|
||||
@@ -206,21 +210,54 @@ a new case does not fit.
|
||||
|
||||
## 2026-08-14 — A formatter in the commit path manufactures drift on a file with a clean git diff
|
||||
|
||||
`apm audit --ci` failed for weeks on `.claude/settings.json` while `git diff` on that file was empty —
|
||||
the worst possible pairing of signals, because the file matched HEAD exactly and every instinct says
|
||||
"nothing changed here". The content was identical to apm's output to the byte; only the JSON key
|
||||
order differed. `pretty-format-json --autofix` sorts object keys unless `--no-sort-keys` is passed,
|
||||
and its `exclude:` listed fifteen generated manifests but not this file, so from the commit that
|
||||
first wrote a hook entry there (`2e395a4`) onward, apm's insertion-ordered output was silently
|
||||
re-sorted on the way in. apm then replayed the install, produced its own order, and reported drift
|
||||
against a file no human had touched.
|
||||
`apm audit --ci` failed on `.claude/settings.json` while `git diff` on that file was empty — the worst
|
||||
possible pairing of signals, because the file matched HEAD exactly and every instinct says "nothing
|
||||
changed here". The content was identical to apm's output to the byte; only the JSON key order
|
||||
differed. `pretty-format-json --autofix` sorts object keys unless `--no-sort-keys` is passed, and its
|
||||
`exclude:` listed fifteen generated manifests but not this file, so from the commit that first wrote
|
||||
a hook entry there onward, apm's insertion-ordered output was silently re-sorted on the way in. apm
|
||||
then replayed the install, produced its own order, and reported drift against a file no human had
|
||||
touched.
|
||||
|
||||
Two general points. First, a tool-owned generated file that passes through an autofixing formatter is
|
||||
drifted by construction, and the diff that would reveal it never appears in `git diff` — it only
|
||||
The provenance matters as much as the mechanism, and the first account of this entry got it wrong in
|
||||
both directions. `git log --format='%h %ad %s' --date=iso` puts the introducing commit `2e395a4` at
|
||||
2026-08-14 18:47 and the fix `7607522` at 21:54 — roughly three hours, not "weeks". And `2e395a4` is
|
||||
the **first commit of the `refactor/trim-skills-agents-context` branch**, eleven minutes after the
|
||||
base merge `f9b919d`; `git branch -a --contains 2e395a4` returns only that branch and its own
|
||||
`remotes/origin/` tracking copy — two lines naming one branch, and `main` is not among them. So
|
||||
this was not a latent defect inherited from `main`, it was manufactured inside the same PR that
|
||||
diagnosed it, and the fixing commit's own message calling it "pre-existing … red at HEAD before
|
||||
ADR-0020 work began" is the mis-attribution rather than the record. Two cheap commands would have
|
||||
settled it before either sentence was written.
|
||||
|
||||
Three general points. First, a tool-owned generated file that passes through an autofixing formatter
|
||||
is drifted by construction, and the diff that would reveal it never appears in `git diff` — it only
|
||||
exists between the formatter's input and its output, which nothing stores. Second, the fix is
|
||||
self-undoing unless the exclude lands in the same commit: correcting the file alone means the hook
|
||||
re-breaks it as it is staged. Fix: when a tool declares ownership of a path, add that path to every
|
||||
autofixing hook's `exclude` at the moment ownership is declared, not when the drift is noticed. This
|
||||
repo gates marketplace-mirror, plugin-content and vale-style drift deterministically and has no
|
||||
equivalent gate asserting tool-owned paths stay out of formatter scope — `.claude/settings.json` was
|
||||
the sixteenth exclude and nothing prevents a seventeenth.
|
||||
re-breaks it as it is staged. Third — the one this entry had to learn twice — "pre-existing" is a
|
||||
claim about history, and history is queryable; a defect found while working on a branch feels
|
||||
inherited, and the feeling is not evidence. A three-hour-old self-inflicted bug and a months-old
|
||||
inherited one call for different responses, and writing the wrong one down converts a process failure
|
||||
into a story about someone else's neglect. Fix: when a tool declares ownership of a path, add that
|
||||
path to every autofixing hook's `exclude` at the moment ownership is declared, not when the drift is
|
||||
noticed — and before describing any defect as pre-existing, run `git log -S` or
|
||||
`git branch --contains` on the commit that introduced it. This repo gates marketplace-mirror,
|
||||
plugin-content and vale-style drift deterministically and has no equivalent gate asserting tool-owned
|
||||
paths stay out of formatter scope — `.claude/settings.json` was the sixteenth exclude and nothing
|
||||
prevents a seventeenth.
|
||||
|
||||
## 2026-08-16 — A rule reversed inside a retrofit leaves no trace unless someone writes it down
|
||||
|
||||
`skill-author/SKILL.md:204` on `main` said "Keep reference chains one level deep — a reference file
|
||||
that references another reference file is rarely loaded correctly." The ADR-0020 retrofit replaced it
|
||||
with "Two hops from `SKILL.md`, never three" in `references/create.md` and `references/retrofit.md`,
|
||||
which permits exactly the chain the old rule banned. The looser rule is the right one and the
|
||||
retrofit could not have shipped without it: dispatch pushes each flow into its own file, so the
|
||||
shipped structure is `SKILL.md` → `improve.md` → `retrofit.md`, and a one-level ceiling would have
|
||||
made the mandatory dispatch pattern illegal. But ADR-0020 says nothing about chain depth, so the
|
||||
reversal was carried entirely by the diff — the new text asserts the new rule with no sign that a
|
||||
contradicting rule ever existed, and a reader who remembers the old one has no way to tell whether it
|
||||
was overturned or overlooked. Fix: when a change inverts a standing authoring rule rather than
|
||||
tightening or restating it, record the inversion where the rule's rationale lives — the ADR if the
|
||||
ADR is the reason, here otherwise. A rule that quietly flips is indistinguishable from a rule that
|
||||
was forgotten, and the second reading is the one that gets it re-added later.
|
||||
@@ -0,0 +1,131 @@
|
||||
# holocron
|
||||
|
||||
The global AI development configuration repository — the authoritative source for agent definitions, skills, workflows, and prompts across all projects. Built as a homelab tool intended to scale to professional environments.
|
||||
|
||||
Content ships as six installable plugins, each an apm (Agent Package Manager) package. This repo consumes its own plugins through apm, so the working copy runs the same released content every other consumer gets.
|
||||
|
||||
## Repo layout
|
||||
|
||||
| Path | What it holds |
|
||||
| --- | --- |
|
||||
| `plugins/` | Six apm packages — `bin`, `core`, `git`, `gitea`, `kyberforge`, `lint` — each carrying skills, and where relevant agents, hooks, MCP servers, and bundled assets |
|
||||
| `providers/claude-code/` | Claude Code adapter, deployed to `~/.claude/` via `scripts/install.sh` |
|
||||
| `core/` | Provider-agnostic always-on content — `core/AGENTS.md` and `core/instructions/` |
|
||||
| `docs/` | Specs (`docs/spec/`), architectural decisions (`docs/adr/`), governance, research, and notes |
|
||||
| `scripts/` | Install, sync, and check scripts used by the git hooks |
|
||||
| `tests/` | `run-tests.sh`, `run-bats.sh`, the `test-*.sh` suites, and the bats submodules |
|
||||
|
||||
The six plugins:
|
||||
|
||||
- **kyberforge** — skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace
|
||||
- **git** — conventional commits, branches, history, submodules, worktrees, remotes, pre-commit hook authoring and running (`pc-author` / `pc-run`), and an interactive router (`git-workflow`)
|
||||
- **gitea** — issues, pull requests, labels, milestones, releases, branches, files, and an interactive router (`gitea-workflow`)
|
||||
- **core** — authoring and auditing a repo's `AGENTS.md` and the provider adapter files that defer to it
|
||||
- **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, compressed output (`caveman`), and re-orienting mid-task (`zoom-out`)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Install all of these before setting up. Each one is a hard dependency of a git hook or a script — several fail with an unhelpful "command not found" if missing.
|
||||
|
||||
| Tool | Why | Install |
|
||||
| --- | --- | --- |
|
||||
| `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`) | The `apm-install` skill, or `curl -sSL https://aka.ms/apm-unix \| sh`. Verify with `apm --version` |
|
||||
| `jq` | Required by `scripts/check-manifests.sh` and `scripts/sync-plugin-content.sh`, both pre-push | Your package manager |
|
||||
| `python3` + PyYAML | Required by `scripts/skill-size-check.sh` (the `skill-size-check` pre-commit hook), which reads folded YAML frontmatter | `python3` is usually present — pre-commit is itself a Python application. `pip install pyyaml` if the hook reports PyYAML missing |
|
||||
| `vale` | Required by the `vale-audit-prefilter-skill` / `-agent` pre-commit hooks and the `check-vale-style-sync` pre-push hook | `brew install vale` (macOS), `snap install vale` (Linux), `choco install vale` (Windows), or https://vale.sh/docs/vale-cli/installation/ |
|
||||
| `claude` CLI | Required by the `validate-plugins` and `validate-marketplace` pre-push hooks | Claude Code |
|
||||
|
||||
Two notes worth reading before you skip one:
|
||||
|
||||
- **PyYAML is a hard requirement, not an optional accelerator.** The hand-rolled fallback frontmatter reader was removed deliberately: a reader that mis-parses an unfamiliar scalar shape reports a clean pass on a file it never measured.
|
||||
- **No `vale sync` is needed.** The `Kyberforge` styles are committed under `plugins/kyberforge/.apm/skills/{skill-audit,agent-audit}/assets/vale/styles/`, not downloaded packages (ADR-0014).
|
||||
|
||||
## Setup
|
||||
|
||||
Run these in order, from the repo root.
|
||||
|
||||
```bash
|
||||
# 1. Deploy this repo's own skills and agents
|
||||
apm install
|
||||
|
||||
# 2. Install the git hooks — all three stages
|
||||
pre-commit install -t pre-commit -t commit-msg -t pre-push
|
||||
```
|
||||
|
||||
**`apm install`** deploys the six plugins into `.claude/skills/` and `.claude/agents/`. Both are gitignored install output, *not* authoring source — `plugins/<name>/.apm/` remains the only place to edit. It needs the network, materializes `apm_modules/` (which stays gitignored), and also configures the `obsidian` MCP server into the repo's `.mcp.json`.
|
||||
|
||||
**Git hooks** must be wired for **all three stages**. This repo's `.pre-commit-config.yaml` has no `default_install_hook_types`, so a plain `pre-commit install` silently skips `commit-msg` (Conventional Commits) and `pre-push` (the full gate) — the `-t` flags above are not optional. The `pc-run` skill handles this and the troubleshooting around it, if you would rather not remember the flags.
|
||||
|
||||
## Keeping the install current
|
||||
|
||||
The six dependencies in root `apm.yml` are unpinned against the default branch, so 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` — an unexplained modification to it after opening a session is expected, not a bug. Commit or discard it deliberately.
|
||||
|
||||
Note the difference between the two commands:
|
||||
|
||||
- `apm install` deploys from `apm.lock.yaml`. It does **not** pick up remote changes.
|
||||
- `apm update` re-resolves refs. This is the command that pulls in a merged `.apm/` edit.
|
||||
|
||||
## Running tests
|
||||
|
||||
```bash
|
||||
bash tests/run-tests.sh # every test-*.sh script plus the bats suite
|
||||
bash tests/run-tests.sh --bats-only # just bats
|
||||
```
|
||||
|
||||
The 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. It *does* fail under `--strict` (equivalently `RUN_TESTS_STRICT=1`), which is how the pre-push hook invokes it — at pre-push, a skip means one of the prerequisites above is absent on this machine, and the gate would otherwise report success having run fewer suites than it appears to. The strict failure names each skipped suite and what to install.
|
||||
|
||||
## Before pushing
|
||||
|
||||
Run the pre-push gate locally in one command:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
**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`:
|
||||
|
||||
```bash
|
||||
SKIP=apm-marketplace-check,apm-pack-check-clean git push
|
||||
```
|
||||
|
||||
Skip **only** those two. The remaining pre-push hooks are real local checks and pass offline; adding one of them to `SKIP` disarms it silently.
|
||||
|
||||
## Editing plugin content
|
||||
|
||||
`plugins/<name>/.apm/` is the only hand-edited source for plugin content — skills, agents, commands, instructions, extensions, and hooks. The flat `plugins/<name>/{skills,agents,commands,instructions,extensions}/` directories, the merged `hooks/hooks.json`, and every `plugin.json` / `marketplace.json` manifest are generated. Nothing labels a generated file as generated, so check the path before you edit; an edit to the mirror is discarded by the next sync and reported as drift by the `check-plugin-content-sync` pre-push hook.
|
||||
|
||||
Hand-authored material that is *not* an `.apm/` primitive — `README.md`, `docs/`, `bin/`, `sources.md`, `.mcp.json` — lives at the plugin **root** and is untouched. Never place such a file inside a mirrored directory: the sync removes the destination before every copy, so it is deleted with no drift report.
|
||||
|
||||
Full detail in [`docs/spec/architecture.md`](docs/spec/architecture.md).
|
||||
|
||||
## For external consumers
|
||||
|
||||
Install a plugin natively from the marketplace manifests:
|
||||
|
||||
```bash
|
||||
claude plugin install <name>@holocron
|
||||
```
|
||||
|
||||
Or consume the packages through apm, the way this repo does — declare them as `dependencies.apm` git+path entries against the holocron remote and run `apm install`.
|
||||
|
||||
## Where to go next
|
||||
|
||||
- [`AGENTS.md`](AGENTS.md) — the rules for AI agents working in this repo
|
||||
- [`CONTEXT.md`](CONTEXT.md) — domain language; read at the start of every session here
|
||||
- [`docs/spec/architecture.md`](docs/spec/architecture.md) — directory structure, install pipeline, provider model
|
||||
- [`docs/spec/gates.md`](docs/spec/gates.md) — the enforcement gates in depth
|
||||
- [`docs/adr/`](docs/adr/) — architectural decisions; read before proposing structural changes
|
||||
- [`docs/VISION.md`](docs/VISION.md) — where this is going
|
||||
- [`LESSONS.md`](LESSONS.md) — things that went wrong once and should not again
|
||||
+279
-71
@@ -1,11 +1,11 @@
|
||||
lockfile_version: '1'
|
||||
generated_at: '2026-08-14T21:26:25.156410+00:00'
|
||||
generated_at: '2026-08-17T06:44:18.931217+00:00'
|
||||
apm_version: 0.28.0
|
||||
dependencies:
|
||||
- repo_url: Defame1297/holocron
|
||||
name: bin
|
||||
host: git.dev.rkdr.net
|
||||
resolved_commit: f9b919d7e3bd5e6b51fbdf88b32ace0438b313e0
|
||||
resolved_commit: 9385c77ac71a67db2b8fe9c3783af9e0fbbf2eae
|
||||
version: 1.1.3
|
||||
virtual_path: plugins/bin
|
||||
is_virtual: true
|
||||
@@ -87,7 +87,7 @@ dependencies:
|
||||
- repo_url: Defame1297/holocron
|
||||
name: core
|
||||
host: git.dev.rkdr.net
|
||||
resolved_commit: f9b919d7e3bd5e6b51fbdf88b32ace0438b313e0
|
||||
resolved_commit: 9385c77ac71a67db2b8fe9c3783af9e0fbbf2eae
|
||||
version: 1.1.1
|
||||
virtual_path: plugins/core
|
||||
is_virtual: true
|
||||
@@ -134,7 +134,7 @@ dependencies:
|
||||
- repo_url: Defame1297/holocron
|
||||
name: git
|
||||
host: git.dev.rkdr.net
|
||||
resolved_commit: f9b919d7e3bd5e6b51fbdf88b32ace0438b313e0
|
||||
resolved_commit: 9385c77ac71a67db2b8fe9c3783af9e0fbbf2eae
|
||||
version: 1.3.3
|
||||
virtual_path: plugins/git
|
||||
is_virtual: true
|
||||
@@ -241,7 +241,7 @@ dependencies:
|
||||
- repo_url: Defame1297/holocron
|
||||
name: gitea
|
||||
host: git.dev.rkdr.net
|
||||
resolved_commit: f9b919d7e3bd5e6b51fbdf88b32ace0438b313e0
|
||||
resolved_commit: 9385c77ac71a67db2b8fe9c3783af9e0fbbf2eae
|
||||
version: 1.3.4
|
||||
virtual_path: plugins/gitea
|
||||
is_virtual: true
|
||||
@@ -332,8 +332,8 @@ dependencies:
|
||||
- repo_url: Defame1297/holocron
|
||||
name: kyberforge
|
||||
host: git.dev.rkdr.net
|
||||
resolved_commit: f9b919d7e3bd5e6b51fbdf88b32ace0438b313e0
|
||||
version: 1.5.0
|
||||
resolved_commit: 9385c77ac71a67db2b8fe9c3783af9e0fbbf2eae
|
||||
version: 1.6.0
|
||||
virtual_path: plugins/kyberforge
|
||||
is_virtual: true
|
||||
package_type: marketplace_plugin
|
||||
@@ -344,15 +344,20 @@ dependencies:
|
||||
- .claude/skills/agent-audit/README.md
|
||||
- .claude/skills/agent-audit/SKILL.md
|
||||
- .claude/skills/agent-audit/assets/vale/.vale.ini
|
||||
- .claude/skills/agent-audit/assets/vale/styles/Kyberforge/CompositionNote.yml
|
||||
- .claude/skills/agent-audit/assets/vale/styles/Kyberforge/DescriptionOpener.yml
|
||||
- .claude/skills/agent-audit/assets/vale/styles/Kyberforge/PaddingPhrase.yml
|
||||
- .claude/skills/agent-audit/assets/vale/styles/Kyberforge/SentenceOpenerThereIs.yml
|
||||
- .claude/skills/agent-audit/assets/vale/styles/Kyberforge/VagueWording.yml
|
||||
- .claude/skills/agent-audit/assets/vale/styles/KyberforgeCopilot/ProactivePhrase.yml
|
||||
- .claude/skills/agent-audit/references/README.md
|
||||
- .claude/skills/agent-audit/references/body-and-delegation.md
|
||||
- .claude/skills/agent-audit/references/description-quality.md
|
||||
- .claude/skills/agent-audit/references/field-inventory.md
|
||||
- .claude/skills/agent-audit/references/scope-plugin-apm.md
|
||||
- .claude/skills/agent-audit/references/scope-project-user.md
|
||||
- .claude/skills/agent-audit/references/sources.md
|
||||
- .claude/skills/agent-audit/references/validation-scripts.md
|
||||
- .claude/skills/agent-audit/scripts/README.md
|
||||
- .claude/skills/agent-audit/scripts/vale-wrap.sh
|
||||
- .claude/skills/agent-audit/scripts/validate-provenance.sh
|
||||
@@ -365,7 +370,12 @@ dependencies:
|
||||
- .claude/skills/agent-author/assets/templates/claude-code.md
|
||||
- .claude/skills/agent-author/assets/templates/copilot.agent.md.template
|
||||
- .claude/skills/agent-author/references/README.md
|
||||
- .claude/skills/agent-author/references/contract.md
|
||||
- .claude/skills/agent-author/references/create.md
|
||||
- .claude/skills/agent-author/references/deployment-modes.md
|
||||
- .claude/skills/agent-author/references/improve.md
|
||||
- .claude/skills/agent-author/references/plugin-scope.md
|
||||
- .claude/skills/agent-author/references/project-user-scope.md
|
||||
- .claude/skills/agent-author/references/scripts.md
|
||||
- .claude/skills/agent-author/references/sources.md
|
||||
- .claude/skills/agent-author/scripts/README.md
|
||||
@@ -391,13 +401,18 @@ dependencies:
|
||||
- .claude/skills/skill-audit/README.md
|
||||
- .claude/skills/skill-audit/SKILL.md
|
||||
- .claude/skills/skill-audit/assets/vale/.vale.ini
|
||||
- .claude/skills/skill-audit/assets/vale/styles/Kyberforge/CompositionNote.yml
|
||||
- .claude/skills/skill-audit/assets/vale/styles/Kyberforge/DescriptionOpener.yml
|
||||
- .claude/skills/skill-audit/assets/vale/styles/Kyberforge/PaddingPhrase.yml
|
||||
- .claude/skills/skill-audit/assets/vale/styles/Kyberforge/SentenceOpenerThereIs.yml
|
||||
- .claude/skills/skill-audit/assets/vale/styles/Kyberforge/VagueWording.yml
|
||||
- .claude/skills/skill-audit/references/body-discipline.md
|
||||
- .claude/skills/skill-audit/references/description-quality.md
|
||||
- .claude/skills/skill-audit/references/file-structure.md
|
||||
- .claude/skills/skill-audit/references/formatting-and-scripts.md
|
||||
- .claude/skills/skill-audit/references/patterns.md
|
||||
- .claude/skills/skill-audit/references/sources.md
|
||||
- .claude/skills/skill-audit/references/validation-scripts.md
|
||||
- .claude/skills/skill-audit/scripts/vale-wrap.sh
|
||||
- .claude/skills/skill-audit/scripts/validate-provenance.sh
|
||||
- .claude/skills/skill-audit/scripts/validate.sh
|
||||
@@ -411,41 +426,55 @@ dependencies:
|
||||
- .claude/skills/skill-author/assets/templates/references/sources.md
|
||||
- .claude/skills/skill-author/assets/templates/scripts/README.md
|
||||
- .claude/skills/skill-author/assets/templates/tests/README.md
|
||||
- .claude/skills/skill-author/references/contract.md
|
||||
- .claude/skills/skill-author/references/create.md
|
||||
- .claude/skills/skill-author/references/deployment-modes.md
|
||||
- .claude/skills/skill-author/references/improve.md
|
||||
- .claude/skills/skill-author/references/retrofit.md
|
||||
- .claude/skills/skill-author/references/scripts.md
|
||||
- .claude/skills/skill-author/references/sources.md
|
||||
- .claude/skills/skill-author/scripts/new-skill.sh
|
||||
deployed_file_hashes:
|
||||
.claude/agents/apm-orchestrate.md: sha256:fbb78f7c8c58b018639e7a39f2f1b3ce2adcd6bed277b3c8f8cd70893698ec73
|
||||
.claude/hooks/kyberforge/.apm/hooks/check-apm-current.sh: sha256:96f44d63b5f4906ac1add5c32b718176ec10c8514615151d7303d9845c607b46
|
||||
.claude/skills/agent-audit/README.md: sha256:a3a63acb80981975330bd9a86d81c8f3e632f7f2d97ac99951710d035fb52953
|
||||
.claude/skills/agent-audit/SKILL.md: sha256:080f391ea25de8afc5a20478cbd382a8d28fcf70a7be7f012033dacb6e9bfed3
|
||||
.claude/skills/agent-audit/README.md: sha256:fafdb84bf258f6d606c97b785c7c378b5de805f0aa8a10d3cedefedb000e4230
|
||||
.claude/skills/agent-audit/SKILL.md: sha256:a126a2a7b1b8c9f6b39272584e2f84577aeeb8c81707be6f151b3acbe35ecf51
|
||||
.claude/skills/agent-audit/assets/vale/.vale.ini: sha256:d643677585c603edcc8816b15d9c85c81b677ded02247b521ba8b88ae5bfdf57
|
||||
.claude/skills/agent-audit/assets/vale/styles/Kyberforge/DescriptionOpener.yml: sha256:a399516a457d40e0af5536a6575c70221909cecac5722eda2788430ebddd8f67
|
||||
.claude/skills/agent-audit/assets/vale/styles/Kyberforge/CompositionNote.yml: sha256:e0a52fb9ff65aee9d21d1f7b61bef0f71d14b8edd9af1d70246715bbb8844ccf
|
||||
.claude/skills/agent-audit/assets/vale/styles/Kyberforge/DescriptionOpener.yml: sha256:60984e0af965137a151a07875f388c94ea2da93d4ee4f2b92811bd14105527f2
|
||||
.claude/skills/agent-audit/assets/vale/styles/Kyberforge/PaddingPhrase.yml: sha256:67b738f5c393a717bdf249fcd946eb7b0d258215bf2240d2be6902711b46d8e9
|
||||
.claude/skills/agent-audit/assets/vale/styles/Kyberforge/SentenceOpenerThereIs.yml: sha256:5dade0238d96730d24bb6e8dba003c3d94694bd9969687f461fe1fada3ca4629
|
||||
.claude/skills/agent-audit/assets/vale/styles/Kyberforge/VagueWording.yml: sha256:d4bf14d0bb2dcbe3a89b66b72a9c9c19d175060b5c4c978da3d67010f1a25083
|
||||
.claude/skills/agent-audit/assets/vale/styles/KyberforgeCopilot/ProactivePhrase.yml: sha256:ba4f91479f66f9f03c74f791cbc6130b24bbc31a25f3897b424f72d02473b9b4
|
||||
.claude/skills/agent-audit/references/README.md: sha256:7664ae08efc8373c0b253c0a5c7a9cc930720c62abb8d31a81b2aedbd573dd30
|
||||
.claude/skills/agent-audit/references/description-quality.md: sha256:79c62fd641d6785a3349d8ff1e207c64547ef9435d51e88518fc9fb5c2f4962e
|
||||
.claude/skills/agent-audit/references/README.md: sha256:f6d777f7d3844d1be847f86f67b08c2db37b2d122e0bf1af92ef057bbfdd1004
|
||||
.claude/skills/agent-audit/references/body-and-delegation.md: sha256:788664bb75364354773bd9efd4a0f84a231009d3a8b080e2561fefff2aa95b69
|
||||
.claude/skills/agent-audit/references/description-quality.md: sha256:15eea332149203ac18c6b3eee98f0078e8a764cdb91b3158590f45204cb3322a
|
||||
.claude/skills/agent-audit/references/field-inventory.md: sha256:e9abd738c08890994441d215eba96f894e00992cb7bb5ce850796186050aa1ae
|
||||
.claude/skills/agent-audit/references/sources.md: sha256:9e06addbfb1a58e4db23eaabe9fb13afe0fffa8419afa575525136cec189c558
|
||||
.claude/skills/agent-audit/references/scope-plugin-apm.md: sha256:af32acf33aec0cd0a98022ee6796265066c693662e8874a8e4d7e03ce1657c88
|
||||
.claude/skills/agent-audit/references/scope-project-user.md: sha256:9ea0d4014d78a6b02c760e42fb6cb72b3441ae6c93991fdeca666857bad05056
|
||||
.claude/skills/agent-audit/references/sources.md: sha256:235de49e805d83ba19f8ecdc90da2f7d3b02c5f1c2fc54179d6bdd5bc398743e
|
||||
.claude/skills/agent-audit/references/validation-scripts.md: sha256:7eb48ec40175d6cce2b3b9602c0d9e60d62bacca1df7cc120c138ea1ce328666
|
||||
.claude/skills/agent-audit/scripts/README.md: sha256:ee427eed6a562a6e86898c69c35508c518d5b3c633ae803e587fbab37514f2b2
|
||||
.claude/skills/agent-audit/scripts/vale-wrap.sh: sha256:c17673b3b0de1c1a49fcb73e2b695835a45014ec1c300a77a3ba7b3c7ce4c995
|
||||
.claude/skills/agent-audit/scripts/validate-provenance.sh: sha256:0e1d2b3bb1abb9f302ed84a618246ebaa7d5f7fda727b290d323e70dcc26a323
|
||||
.claude/skills/agent-audit/scripts/validate.sh: sha256:29727875f419b8d3b6c76f157b7569adb4d2b9a88f01eeb2c09e04dfb8541cb3
|
||||
.claude/skills/agent-author/README.md: sha256:015a6b030ff2aa913f982a223f8afad789edadc4e7148d3b1740a56b346c658d
|
||||
.claude/skills/agent-author/SKILL.md: sha256:39193473a9b1beacb4790cab4e5e31e01f7021a68a5c4cc16a606420bee65d6a
|
||||
.claude/skills/agent-author/assets/README.md: sha256:a82665201ead91b7fa2b37629327ecb89ca785b7f2b261300c2dd816b4219bcd
|
||||
.claude/skills/agent-author/assets/templates/apm-agent.md: sha256:c2693395d7ec72645fc1b79c65039ddadc0077f5856de3277d600bde41f17e54
|
||||
.claude/skills/agent-author/assets/templates/claude-code.md: sha256:eef5ee46d93b6896e880b3c4aec1121c4077a2556c6d9f4a01b322afc7b63a7e
|
||||
.claude/skills/agent-author/assets/templates/copilot.agent.md.template: sha256:1e0c0445a8908148e69baeeb44316fb370aa483e17fcd63d7f1b2d3ad1927788
|
||||
.claude/skills/agent-author/references/README.md: sha256:bb4cf7a34c512da33f574b208756941f396f4a1e55daaed0fad22569a22af301
|
||||
.claude/skills/agent-author/references/deployment-modes.md: sha256:6cd010455e10574566ebf7cfd0926217ae5812d56a62daf4bcdb325abf3b0421
|
||||
.claude/skills/agent-audit/scripts/validate.sh: sha256:50c6d3875fd44901fba3fcb69256f616be19521ee8209b7132778c1d4a693a3f
|
||||
.claude/skills/agent-author/README.md: sha256:b2d14a3c8fc3ddc3935d0bd1765192b333b944baaf9bfa9a350bef0985890265
|
||||
.claude/skills/agent-author/SKILL.md: sha256:8fe96e4744378cd33876cf87a08a20af001898e89f0f27087044e1b87eac61f2
|
||||
.claude/skills/agent-author/assets/README.md: sha256:730b2321b5f16dcc63fc5dc842618c116afc75217ae925057dafc490e7d57644
|
||||
.claude/skills/agent-author/assets/templates/apm-agent.md: sha256:9b57e2b93a5705388351f14294d60833325d9596cf5294d22f4a586165fc35fa
|
||||
.claude/skills/agent-author/assets/templates/claude-code.md: sha256:ea0bac6e8cd91c767c6114cbcf1ecd124d03abff06ba81696dac5115499c395a
|
||||
.claude/skills/agent-author/assets/templates/copilot.agent.md.template: sha256:ee8b7585c808c5ff4b715be74d9e4f8c5c17d13cd603a8727cd8ca1793d14760
|
||||
.claude/skills/agent-author/references/README.md: sha256:aca156b02a3f0b9fddde3cefdb24370e17cc5886dcd78adcb382699a5cc0bbaf
|
||||
.claude/skills/agent-author/references/contract.md: sha256:a96f0ef021a6629120e5af7c98fa483d31de92cea37f98b51528a28dee38e16a
|
||||
.claude/skills/agent-author/references/create.md: sha256:e4a1b95970ec44d551a4b0598f2fe01463db13a6b118981d8cd03dfc7e24f451
|
||||
.claude/skills/agent-author/references/deployment-modes.md: sha256:5675f8dcd1523269d5c65be5b0e06ff4d822b158050e2fdd72b54f605485ab1f
|
||||
.claude/skills/agent-author/references/improve.md: sha256:f260e91f28bbd62093b53f4efc05821eff00d086f834ddb083b70dd77ddffcaa
|
||||
.claude/skills/agent-author/references/plugin-scope.md: sha256:a20f8aff55c9d1c1ba5e21054b81da80618270b3b6ca5bfc37ae3c2089b4b916
|
||||
.claude/skills/agent-author/references/project-user-scope.md: sha256:3f686736259402e38fed29f215b7aa5a9529fc9025005f82d0c3d439958e4e03
|
||||
.claude/skills/agent-author/references/scripts.md: sha256:290f5d8ab0a4f2073e14f7139fd55ab40310137f08ca7a2d59ed35a53488a592
|
||||
.claude/skills/agent-author/references/sources.md: sha256:56965f9b660fc9a32dd71ca1039b4c6b8f932420628ddfbf3a2525e7c3336ce4
|
||||
.claude/skills/agent-author/references/sources.md: sha256:cac8baf85a6d958ed2f0f882446dc123d14082d5ca412c8edee12c6cd1b6fdc3
|
||||
.claude/skills/agent-author/scripts/README.md: sha256:e8ea2f3391ca297afeda03911768000617a71df0cf9d673bbb260ee5bbfed7c2
|
||||
.claude/skills/agent-author/scripts/new-agent.sh: sha256:e917055957500a72c7f684810552ba8931781c90f96dedaabc07e899dae02a89
|
||||
.claude/skills/agent-author/scripts/new-agent.sh: sha256:1c631f5178c6c0c397d7ceefb606892dd7e330cd1b22a03c1e4d11e85fbdb7de
|
||||
.claude/skills/apm-install/README.md: sha256:d7bd79ee27997fffb5765c03358a700d8c00266ea120f6d067d74300d6c26eb0
|
||||
.claude/skills/apm-install/SKILL.md: sha256:4c0db5454bcbf895a89e7405eeefb4521a2b23f2ff28aa96c82c59708d8819ee
|
||||
.claude/skills/apm-install/references/sources.md: sha256:b80245ff8ae179d458572cb9b03621d00bf7e2f052f0cd71b88112c0ea8a91ce
|
||||
@@ -460,39 +489,48 @@ dependencies:
|
||||
.claude/skills/forge/README.md: sha256:2151b952f4438cd9212edd97de90eec06f027f01121b3badb9d32dc9c45320c4
|
||||
.claude/skills/forge/SKILL.md: sha256:4de4c629cb4aec4c6cac5347163a86fe123299f131510dc43857597ded6ae497
|
||||
.claude/skills/forge/references/sources.md: sha256:a7f614c0ab37aa403545e191722bca012836162dcab30ff8ac984cbb25ad68cd
|
||||
.claude/skills/skill-audit/README.md: sha256:9e3533c3c44cfd249cf02684ef43db822ed41382362e8ce71f1254d10711c8e0
|
||||
.claude/skills/skill-audit/SKILL.md: sha256:dbe2adfd0101cf9d6b3c2c3c2aa3ae397c717f835c1059c0f1d805e9057dc397
|
||||
.claude/skills/skill-audit/README.md: sha256:78bfcfb16bbeef814b83f37a4a5185886003f9442a6d0e7e94be56f54d18d7a5
|
||||
.claude/skills/skill-audit/SKILL.md: sha256:57a9336d409c883dc9a78aa92a9f0075fac6f8e4aac6ee8fdab705055ef85162
|
||||
.claude/skills/skill-audit/assets/vale/.vale.ini: sha256:0d1108b17a941b514dd9a62a7382d1febdd543118750246c81fba406440a9f9f
|
||||
.claude/skills/skill-audit/assets/vale/styles/Kyberforge/DescriptionOpener.yml: sha256:a399516a457d40e0af5536a6575c70221909cecac5722eda2788430ebddd8f67
|
||||
.claude/skills/skill-audit/assets/vale/styles/Kyberforge/CompositionNote.yml: sha256:e0a52fb9ff65aee9d21d1f7b61bef0f71d14b8edd9af1d70246715bbb8844ccf
|
||||
.claude/skills/skill-audit/assets/vale/styles/Kyberforge/DescriptionOpener.yml: sha256:60984e0af965137a151a07875f388c94ea2da93d4ee4f2b92811bd14105527f2
|
||||
.claude/skills/skill-audit/assets/vale/styles/Kyberforge/PaddingPhrase.yml: sha256:67b738f5c393a717bdf249fcd946eb7b0d258215bf2240d2be6902711b46d8e9
|
||||
.claude/skills/skill-audit/assets/vale/styles/Kyberforge/SentenceOpenerThereIs.yml: sha256:5dade0238d96730d24bb6e8dba003c3d94694bd9969687f461fe1fada3ca4629
|
||||
.claude/skills/skill-audit/assets/vale/styles/Kyberforge/VagueWording.yml: sha256:d4bf14d0bb2dcbe3a89b66b72a9c9c19d175060b5c4c978da3d67010f1a25083
|
||||
.claude/skills/skill-audit/references/body-discipline.md: sha256:2295d2fc72fa529f3700c21fa93516ad186a7cc66d470370173ab1a784d03d31
|
||||
.claude/skills/skill-audit/references/description-quality.md: sha256:ab077e71bbc6e9d8e2a1384bedaf613a4219f28bbe42eb9e89f933b579db6b33
|
||||
.claude/skills/skill-audit/references/sources.md: sha256:cb65dd3cc411d7b264fab24de5cd2353dec8fbb7bbcfe2388535d963be240280
|
||||
.claude/skills/skill-audit/references/body-discipline.md: sha256:f8a27d7266f4453781712f0bead151580158581a445a641613203a67a1c337d7
|
||||
.claude/skills/skill-audit/references/description-quality.md: sha256:2d37eccb4889cade6cbad0deaaed6d1f5fe4c3276c5438c244b2f87ae27bf432
|
||||
.claude/skills/skill-audit/references/file-structure.md: sha256:b24638bd639ef048da171f6dc711e345749cde3c70ec6ba1b7fcbd696b921841
|
||||
.claude/skills/skill-audit/references/formatting-and-scripts.md: sha256:5c319866b2f22ba07789dae7b0813132f67196c508e8da23971307c2a9e57095
|
||||
.claude/skills/skill-audit/references/patterns.md: sha256:4a942ab30b95dc0cbf0fbe7da3b91157f166bce80cbd17bcf60dc365ea9237af
|
||||
.claude/skills/skill-audit/references/sources.md: sha256:1db16ea23884ac2db799b42d1284daaab4b24eb94f184a95fb67ddc280385934
|
||||
.claude/skills/skill-audit/references/validation-scripts.md: sha256:44f3b82413b57f2b5671637bb449a4c01b6d5fcd9059245aaa38e45108adf480
|
||||
.claude/skills/skill-audit/scripts/vale-wrap.sh: sha256:c17673b3b0de1c1a49fcb73e2b695835a45014ec1c300a77a3ba7b3c7ce4c995
|
||||
.claude/skills/skill-audit/scripts/validate-provenance.sh: sha256:015376c5ad1f6bb6b53549537317a55e3ac06cd4e347451dba85879a3928ccbe
|
||||
.claude/skills/skill-audit/scripts/validate.sh: sha256:ebbef859276135b7fe4189b31396fd98e5d4019685d7cb55e39a10fac8ca546d
|
||||
.claude/skills/skill-author/README.md: sha256:6c0b36f9b28a33de1646df45bd8e4d733ab3b26c0bdff49743962aa068085f6d
|
||||
.claude/skills/skill-author/SKILL.md: sha256:7b82d16fe0d61a71c5b59a244cf92b9b5fc2635916ad1572f4f5f6e37172bf09
|
||||
.claude/skills/skill-audit/scripts/validate.sh: sha256:7ce27df82e4160c6296db6c8314865f022da5c4334151857570a86167c8d4e11
|
||||
.claude/skills/skill-author/README.md: sha256:d22035d302818e103486e447bd92d2da4c9b73da60f30157941710f32f400b89
|
||||
.claude/skills/skill-author/SKILL.md: sha256:615a936580f44dd8e523496aadf88df6d512ac58369190b90407e8fca86e8345
|
||||
.claude/skills/skill-author/assets/templates/README.md: sha256:f7e91356f4c85862f96a9f46a7a7ae8b5dc715c8174add5cf070166a8bae0658
|
||||
.claude/skills/skill-author/assets/templates/SKILL.md: sha256:f05a127963874bfbbe4333c42c5208736d84d5bf12dd8478351f6853304cc9d1
|
||||
.claude/skills/skill-author/assets/templates/SKILL.md: sha256:b20c65bd185b300de7e65d16decdf0ab4aaf6c094822c1f798b3c278d52abbff
|
||||
.claude/skills/skill-author/assets/templates/assets/README.md: sha256:b72e51e643c45ded210a257e475d5661035d2aa5ed581e3294fd2aed00765c4b
|
||||
.claude/skills/skill-author/assets/templates/references/README.md: sha256:63754636f84a7841f31ab1f2854025ee449a29840ed6b9b51bcb1422ab5522d0
|
||||
.claude/skills/skill-author/assets/templates/references/README.md: sha256:e2ef117972ab2625a456ed80ff085859666d145de4e270f383cde7c8e84ad60a
|
||||
.claude/skills/skill-author/assets/templates/references/sources.md: sha256:fe126720ba890b98e829f2626e3f5a361a0c2ce47eda97bd3f7dc1812ddd62b7
|
||||
.claude/skills/skill-author/assets/templates/scripts/README.md: sha256:ee427eed6a562a6e86898c69c35508c518d5b3c633ae803e587fbab37514f2b2
|
||||
.claude/skills/skill-author/assets/templates/tests/README.md: sha256:5d331de121105ca79b544b9d3779a316a64cee46fa8d51ba0ea789ab5c233442
|
||||
.claude/skills/skill-author/references/contract.md: sha256:d350721118a2bd57094fa9151f11b48a663a1c8a04e50fba71ff2baf64b7282f
|
||||
.claude/skills/skill-author/references/create.md: sha256:0837a77ad6da7ae9894134d15eebd57950b8505e8bd2b32ee69f281d65a6abcb
|
||||
.claude/skills/skill-author/references/deployment-modes.md: sha256:ffe2d928f2b5ec90c509f4d4cc5bd33c71899852cb5cc5a31fe1513b707fb759
|
||||
.claude/skills/skill-author/references/improve.md: sha256:abe029587ce475fed165d8fcfa46ea1abe83f90727d093f8121a9a7a7daa5a3d
|
||||
.claude/skills/skill-author/references/retrofit.md: sha256:6c8336caa717f2abd38cb1f9c776479c0e908f7dbf577021d3689b375287dc77
|
||||
.claude/skills/skill-author/references/scripts.md: sha256:fe71da1fb3d947846ad1a37348f90866b0c4f7dbd81331cfe3300981656f6cc2
|
||||
.claude/skills/skill-author/references/sources.md: sha256:27637acad4c0cdc7f1db15bc9cab339ebdac643e084642c58945ac2f81020c75
|
||||
.claude/skills/skill-author/scripts/new-skill.sh: sha256:46c6903404b9eabbd6c83892213fdabf9cad193be64613b8c1b84c8dc37e8eb2
|
||||
content_hash: sha256:c89a45409fd3e06a6c63ca8a8f0f0d9c8e10bbded73fdef15dd482eebc6b6a78
|
||||
.claude/skills/skill-author/references/sources.md: sha256:652f7ce26c0d68a3ed11db09206bff20ba230ca8ff1fe71ae45b6f4505979327
|
||||
.claude/skills/skill-author/scripts/new-skill.sh: sha256:c788b56f73ed4fd03edb179159361fe16e3afbf528a9895e9251909c5aa40a7f
|
||||
content_hash: sha256:32185a11d3d859e5235de2c43065920ba8950c1684e6aec46f0f7170ff4a111e
|
||||
declared_license: MIT
|
||||
exec_status: deployed
|
||||
- repo_url: Defame1297/holocron
|
||||
name: lint
|
||||
host: git.dev.rkdr.net
|
||||
resolved_commit: f9b919d7e3bd5e6b51fbdf88b32ace0438b313e0
|
||||
resolved_commit: 9385c77ac71a67db2b8fe9c3783af9e0fbbf2eae
|
||||
version: 1.1.6
|
||||
virtual_path: plugins/lint
|
||||
is_virtual: true
|
||||
@@ -519,9 +557,8 @@ dependencies:
|
||||
.claude/skills/vale-run/SKILL.md: sha256:16c42c97de14ef20296b9dfce46b0e2f9be6ad68751c35975251cecff80f79d9
|
||||
.claude/skills/vale-run/references/sources.md: sha256:1f46b727e5f3db09d6c8c01c8a615bad60ce2c5b3a01226e1a3fd3b933330bc7
|
||||
.claude/skills/vale-run/references/troubleshooting.md: sha256:b4c2bc67b413b102d9fe1cd7e5a248b19aba3dc3b60b4429e777977cf8021cd5
|
||||
content_hash: sha256:7bc57b8852680bdcba32a6a6108b99ef6bcdf26eacfecd806c421ec5d1fd47d9
|
||||
content_hash: sha256:f7915119bf5d349bb4dd26b7999278770dd2e0b38e4b4db57c0819a15cbf97a3
|
||||
declared_license: MIT
|
||||
exec_status: gated_pending_approval
|
||||
deployments:
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
@@ -585,7 +622,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:a3a63acb80981975330bd9a86d81c8f3e632f7f2d97ac99951710d035fb52953
|
||||
content_hash: sha256:fafdb84bf258f6d606c97b785c7c378b5de805f0aa8a10d3cedefedb000e4230
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-audit/SKILL.md
|
||||
@@ -594,7 +631,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:080f391ea25de8afc5a20478cbd382a8d28fcf70a7be7f012033dacb6e9bfed3
|
||||
content_hash: sha256:a126a2a7b1b8c9f6b39272584e2f84577aeeb8c81707be6f151b3acbe35ecf51
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-audit/assets/vale/.vale.ini
|
||||
@@ -604,6 +641,15 @@ deployments:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:d643677585c603edcc8816b15d9c85c81b677ded02247b521ba8b88ae5bfdf57
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-audit/assets/vale/styles/Kyberforge/CompositionNote.yml
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:e0a52fb9ff65aee9d21d1f7b61bef0f71d14b8edd9af1d70246715bbb8844ccf
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-audit/assets/vale/styles/Kyberforge/DescriptionOpener.yml
|
||||
@@ -612,7 +658,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:a399516a457d40e0af5536a6575c70221909cecac5722eda2788430ebddd8f67
|
||||
content_hash: sha256:60984e0af965137a151a07875f388c94ea2da93d4ee4f2b92811bd14105527f2
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-audit/assets/vale/styles/Kyberforge/PaddingPhrase.yml
|
||||
@@ -657,7 +703,16 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:7664ae08efc8373c0b253c0a5c7a9cc930720c62abb8d31a81b2aedbd573dd30
|
||||
content_hash: sha256:f6d777f7d3844d1be847f86f67b08c2db37b2d122e0bf1af92ef057bbfdd1004
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-audit/references/body-and-delegation.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:788664bb75364354773bd9efd4a0f84a231009d3a8b080e2561fefff2aa95b69
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-audit/references/description-quality.md
|
||||
@@ -666,7 +721,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:79c62fd641d6785a3349d8ff1e207c64547ef9435d51e88518fc9fb5c2f4962e
|
||||
content_hash: sha256:15eea332149203ac18c6b3eee98f0078e8a764cdb91b3158590f45204cb3322a
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-audit/references/field-inventory.md
|
||||
@@ -676,6 +731,24 @@ deployments:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:e9abd738c08890994441d215eba96f894e00992cb7bb5ce850796186050aa1ae
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-audit/references/scope-plugin-apm.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:af32acf33aec0cd0a98022ee6796265066c693662e8874a8e4d7e03ce1657c88
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-audit/references/scope-project-user.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:9ea0d4014d78a6b02c760e42fb6cb72b3441ae6c93991fdeca666857bad05056
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-audit/references/sources.md
|
||||
@@ -684,7 +757,16 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:9e06addbfb1a58e4db23eaabe9fb13afe0fffa8419afa575525136cec189c558
|
||||
content_hash: sha256:235de49e805d83ba19f8ecdc90da2f7d3b02c5f1c2fc54179d6bdd5bc398743e
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-audit/references/validation-scripts.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:7eb48ec40175d6cce2b3b9602c0d9e60d62bacca1df7cc120c138ea1ce328666
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-audit/scripts/README.md
|
||||
@@ -720,7 +802,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:29727875f419b8d3b6c76f157b7569adb4d2b9a88f01eeb2c09e04dfb8541cb3
|
||||
content_hash: sha256:50c6d3875fd44901fba3fcb69256f616be19521ee8209b7132778c1d4a693a3f
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-author
|
||||
@@ -738,7 +820,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:015a6b030ff2aa913f982a223f8afad789edadc4e7148d3b1740a56b346c658d
|
||||
content_hash: sha256:b2d14a3c8fc3ddc3935d0bd1765192b333b944baaf9bfa9a350bef0985890265
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-author/SKILL.md
|
||||
@@ -747,7 +829,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:39193473a9b1beacb4790cab4e5e31e01f7021a68a5c4cc16a606420bee65d6a
|
||||
content_hash: sha256:8fe96e4744378cd33876cf87a08a20af001898e89f0f27087044e1b87eac61f2
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-author/assets/README.md
|
||||
@@ -756,7 +838,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:a82665201ead91b7fa2b37629327ecb89ca785b7f2b261300c2dd816b4219bcd
|
||||
content_hash: sha256:730b2321b5f16dcc63fc5dc842618c116afc75217ae925057dafc490e7d57644
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-author/assets/templates/apm-agent.md
|
||||
@@ -765,7 +847,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:c2693395d7ec72645fc1b79c65039ddadc0077f5856de3277d600bde41f17e54
|
||||
content_hash: sha256:9b57e2b93a5705388351f14294d60833325d9596cf5294d22f4a586165fc35fa
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-author/assets/templates/claude-code.md
|
||||
@@ -774,7 +856,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:eef5ee46d93b6896e880b3c4aec1121c4077a2556c6d9f4a01b322afc7b63a7e
|
||||
content_hash: sha256:ea0bac6e8cd91c767c6114cbcf1ecd124d03abff06ba81696dac5115499c395a
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-author/assets/templates/copilot.agent.md.template
|
||||
@@ -783,7 +865,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:1e0c0445a8908148e69baeeb44316fb370aa483e17fcd63d7f1b2d3ad1927788
|
||||
content_hash: sha256:ee8b7585c808c5ff4b715be74d9e4f8c5c17d13cd603a8727cd8ca1793d14760
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-author/references/README.md
|
||||
@@ -792,7 +874,25 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:bb4cf7a34c512da33f574b208756941f396f4a1e55daaed0fad22569a22af301
|
||||
content_hash: sha256:aca156b02a3f0b9fddde3cefdb24370e17cc5886dcd78adcb382699a5cc0bbaf
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-author/references/contract.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:a96f0ef021a6629120e5af7c98fa483d31de92cea37f98b51528a28dee38e16a
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-author/references/create.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:e4a1b95970ec44d551a4b0598f2fe01463db13a6b118981d8cd03dfc7e24f451
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-author/references/deployment-modes.md
|
||||
@@ -801,7 +901,34 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:6cd010455e10574566ebf7cfd0926217ae5812d56a62daf4bcdb325abf3b0421
|
||||
content_hash: sha256:5675f8dcd1523269d5c65be5b0e06ff4d822b158050e2fdd72b54f605485ab1f
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-author/references/improve.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:f260e91f28bbd62093b53f4efc05821eff00d086f834ddb083b70dd77ddffcaa
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-author/references/plugin-scope.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:a20f8aff55c9d1c1ba5e21054b81da80618270b3b6ca5bfc37ae3c2089b4b916
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-author/references/project-user-scope.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:3f686736259402e38fed29f215b7aa5a9529fc9025005f82d0c3d439958e4e03
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-author/references/scripts.md
|
||||
@@ -819,7 +946,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:56965f9b660fc9a32dd71ca1039b4c6b8f932420628ddfbf3a2525e7c3336ce4
|
||||
content_hash: sha256:cac8baf85a6d958ed2f0f882446dc123d14082d5ca412c8edee12c6cd1b6fdc3
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agent-author/scripts/README.md
|
||||
@@ -837,7 +964,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:e917055957500a72c7f684810552ba8931781c90f96dedaabc07e899dae02a89
|
||||
content_hash: sha256:1c631f5178c6c0c397d7ceefb606892dd7e330cd1b22a03c1e4d11e85fbdb7de
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/agentsmd-audit
|
||||
@@ -2241,7 +2368,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:9e3533c3c44cfd249cf02684ef43db822ed41382362e8ce71f1254d10711c8e0
|
||||
content_hash: sha256:78bfcfb16bbeef814b83f37a4a5185886003f9442a6d0e7e94be56f54d18d7a5
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-audit/SKILL.md
|
||||
@@ -2250,7 +2377,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:dbe2adfd0101cf9d6b3c2c3c2aa3ae397c717f835c1059c0f1d805e9057dc397
|
||||
content_hash: sha256:57a9336d409c883dc9a78aa92a9f0075fac6f8e4aac6ee8fdab705055ef85162
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-audit/assets/vale/.vale.ini
|
||||
@@ -2260,6 +2387,15 @@ deployments:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:0d1108b17a941b514dd9a62a7382d1febdd543118750246c81fba406440a9f9f
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-audit/assets/vale/styles/Kyberforge/CompositionNote.yml
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:e0a52fb9ff65aee9d21d1f7b61bef0f71d14b8edd9af1d70246715bbb8844ccf
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-audit/assets/vale/styles/Kyberforge/DescriptionOpener.yml
|
||||
@@ -2268,7 +2404,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:a399516a457d40e0af5536a6575c70221909cecac5722eda2788430ebddd8f67
|
||||
content_hash: sha256:60984e0af965137a151a07875f388c94ea2da93d4ee4f2b92811bd14105527f2
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-audit/assets/vale/styles/Kyberforge/PaddingPhrase.yml
|
||||
@@ -2304,7 +2440,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:2295d2fc72fa529f3700c21fa93516ad186a7cc66d470370173ab1a784d03d31
|
||||
content_hash: sha256:f8a27d7266f4453781712f0bead151580158581a445a641613203a67a1c337d7
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-audit/references/description-quality.md
|
||||
@@ -2313,7 +2449,34 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:ab077e71bbc6e9d8e2a1384bedaf613a4219f28bbe42eb9e89f933b579db6b33
|
||||
content_hash: sha256:2d37eccb4889cade6cbad0deaaed6d1f5fe4c3276c5438c244b2f87ae27bf432
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-audit/references/file-structure.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:b24638bd639ef048da171f6dc711e345749cde3c70ec6ba1b7fcbd696b921841
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-audit/references/formatting-and-scripts.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:5c319866b2f22ba07789dae7b0813132f67196c508e8da23971307c2a9e57095
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-audit/references/patterns.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:4a942ab30b95dc0cbf0fbe7da3b91157f166bce80cbd17bcf60dc365ea9237af
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-audit/references/sources.md
|
||||
@@ -2322,7 +2485,16 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:cb65dd3cc411d7b264fab24de5cd2353dec8fbb7bbcfe2388535d963be240280
|
||||
content_hash: sha256:1db16ea23884ac2db799b42d1284daaab4b24eb94f184a95fb67ddc280385934
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-audit/references/validation-scripts.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:44f3b82413b57f2b5671637bb449a4c01b6d5fcd9059245aaa38e45108adf480
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-audit/scripts/vale-wrap.sh
|
||||
@@ -2349,7 +2521,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:ebbef859276135b7fe4189b31396fd98e5d4019685d7cb55e39a10fac8ca546d
|
||||
content_hash: sha256:7ce27df82e4160c6296db6c8314865f022da5c4334151857570a86167c8d4e11
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-author
|
||||
@@ -2367,7 +2539,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:6c0b36f9b28a33de1646df45bd8e4d733ab3b26c0bdff49743962aa068085f6d
|
||||
content_hash: sha256:d22035d302818e103486e447bd92d2da4c9b73da60f30157941710f32f400b89
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-author/SKILL.md
|
||||
@@ -2376,7 +2548,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:7b82d16fe0d61a71c5b59a244cf92b9b5fc2635916ad1572f4f5f6e37172bf09
|
||||
content_hash: sha256:615a936580f44dd8e523496aadf88df6d512ac58369190b90407e8fca86e8345
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-author/assets/templates/README.md
|
||||
@@ -2394,7 +2566,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:f05a127963874bfbbe4333c42c5208736d84d5bf12dd8478351f6853304cc9d1
|
||||
content_hash: sha256:b20c65bd185b300de7e65d16decdf0ab4aaf6c094822c1f798b3c278d52abbff
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-author/assets/templates/assets/README.md
|
||||
@@ -2412,7 +2584,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:63754636f84a7841f31ab1f2854025ee449a29840ed6b9b51bcb1422ab5522d0
|
||||
content_hash: sha256:e2ef117972ab2625a456ed80ff085859666d145de4e270f383cde7c8e84ad60a
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-author/assets/templates/references/sources.md
|
||||
@@ -2440,6 +2612,24 @@ deployments:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:5d331de121105ca79b544b9d3779a316a64cee46fa8d51ba0ea789ab5c233442
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-author/references/contract.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:d350721118a2bd57094fa9151f11b48a663a1c8a04e50fba71ff2baf64b7282f
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-author/references/create.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:0837a77ad6da7ae9894134d15eebd57950b8505e8bd2b32ee69f281d65a6abcb
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-author/references/deployment-modes.md
|
||||
@@ -2449,6 +2639,24 @@ deployments:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:ffe2d928f2b5ec90c509f4d4cc5bd33c71899852cb5cc5a31fe1513b707fb759
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-author/references/improve.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:abe029587ce475fed165d8fcfa46ea1abe83f90727d093f8121a9a7a7daa5a3d
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-author/references/retrofit.md
|
||||
runtime: null
|
||||
scope: project
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:6c8336caa717f2abd38cb1f9c776479c0e908f7dbf577021d3689b375287dc77
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-author/references/scripts.md
|
||||
@@ -2466,7 +2674,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:27637acad4c0cdc7f1db15bc9cab339ebdac643e084642c58945ac2f81020c75
|
||||
content_hash: sha256:652f7ce26c0d68a3ed11db09206bff20ba230ca8ff1fe71ae45b6f4505979327
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/skill-author/scripts/new-skill.sh
|
||||
@@ -2475,7 +2683,7 @@ deployments:
|
||||
owners:
|
||||
- git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
active_owner: git.dev.rkdr.net/Defame1297/holocron/plugins/kyberforge
|
||||
content_hash: sha256:46c6903404b9eabbd6c83892213fdabf9cad193be64613b8c1b84c8dc37e8eb2
|
||||
content_hash: sha256:c788b56f73ed4fd03edb179159361fe16e3afbf528a9895e9251909c5aa40a7f
|
||||
- kind: project-relative
|
||||
target: claude
|
||||
value: .claude/skills/tdd
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: holocron
|
||||
version: 0.4.1
|
||||
version: 0.4.5
|
||||
description: AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.
|
||||
license: MIT
|
||||
|
||||
@@ -42,7 +42,7 @@ dependencies:
|
||||
# after a kyberforge release, check this first.
|
||||
executables:
|
||||
allow:
|
||||
kyberforge#1.5.0:
|
||||
kyberforge#1.6.0:
|
||||
hooks: true
|
||||
bin: true
|
||||
|
||||
@@ -52,7 +52,7 @@ marketplace:
|
||||
# top-level apm.yml description:/version: above are NOT inherited into the
|
||||
# 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.
|
||||
version: 0.4.1
|
||||
version: 0.4.5
|
||||
owner:
|
||||
name: Defame1297
|
||||
email: [email protected]
|
||||
@@ -79,25 +79,25 @@ marketplace:
|
||||
- name: kyberforge
|
||||
description: Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.
|
||||
source: ./plugins/kyberforge
|
||||
version: 1.5.0
|
||||
version: 1.6.0
|
||||
category: Developer Tools
|
||||
|
||||
- name: bin
|
||||
description: A place for things to be binned
|
||||
description: Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.
|
||||
source: ./plugins/bin
|
||||
version: 1.1.3
|
||||
version: 1.1.5
|
||||
category: Utilities
|
||||
|
||||
- name: git
|
||||
description: Skills for working with Git — conventional commits, branch management, pull requests, and feature flow.
|
||||
description: Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.
|
||||
source: ./plugins/git
|
||||
version: 1.3.3
|
||||
version: 1.3.5
|
||||
category: Version Control
|
||||
|
||||
- name: gitea
|
||||
description: Skills for managing Gitea repositories — issues, pull requests, milestones, releases, and wikis.
|
||||
description: Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.
|
||||
source: ./plugins/gitea
|
||||
version: 1.3.4
|
||||
version: 1.3.6
|
||||
category: Version Control
|
||||
|
||||
- name: core
|
||||
|
||||
@@ -18,4 +18,4 @@ Three alternatives were rejected. Keeping the file-based fallback adds code comp
|
||||
|
||||
The file-based model also had a structural weakness: issues in `docs/issues/` were invisible from the Gitea UI, making it impossible to track work, assign milestones, or filter by label without opening the repo locally. Gitea provides all of that natively.
|
||||
|
||||
The "Provider-agnostic issue tracker" glossary entry in CONTEXT.md is updated in the same workstream to remove the file-based phase framing. The `providers/gitea/` adapter path described in ADR-0011 was never implemented — Gitea integration runs entirely via MCP, not a provider adapter.
|
||||
The "Provider-agnostic issue tracker" glossary entry in CONTEXT.md is updated in the same workstream to remove the file-based phase framing. (Amended 2026-08-17: the CONTEXT.md trim renamed that entry to **Issue**; it still records Gitea as this repo's canonical tracker and still tells skills to say "linked issue" generically.) The `providers/gitea/` adapter path described in ADR-0011 was never implemented — Gitea integration runs entirely via MCP, not a provider adapter.
|
||||
@@ -9,6 +9,12 @@ deferred PR #85 review item to broaden that coverage, retroactively captures #84
|
||||
(since it was never recorded as a decision in its own right), and layers the expansion on top
|
||||
without reversing or weakening the original four rules.
|
||||
|
||||
**2026-08-17 amendment.** The CONTEXT.md section named above no longer holds that documentation.
|
||||
CONTEXT.md was cut back to a glossary and the prefilter's mechanics — the two-copy style layout,
|
||||
`vale-wrap.sh`, the `--config` argv defect, the rule inventory, and the 0-files-means-NOT-RUN
|
||||
fallback — moved to `docs/spec/gates.md`. Read that file, not CONTEXT.md, for the harness itself;
|
||||
this ADR still owns the scope decision.
|
||||
|
||||
**File scope stays the same.** `SKILL.md` plus agent files (`**/agents/*.md`,
|
||||
`**/*.agent.md`) only — matching the existing prefilter's globs. Skill-level
|
||||
`README.md` files and `plugin.json` manifests are not added: README.md files are navigational, not
|
||||
|
||||
@@ -56,6 +56,10 @@ new hand-maintained manifest format.
|
||||
that work through to merge.
|
||||
- `CONTEXT.md`'s "Plugin"/"Plugin marketplace" glossary entries were rewritten in issue #90 to
|
||||
describe the compiled-output model directly, rather than carrying a forward-pointer to this ADR.
|
||||
Superseded 2026-08-17: CONTEXT.md was cut back to one-line definitions, and the compiled-output
|
||||
model is now described in `docs/spec/architecture.md`. The same trim deleted the "lint plugin"
|
||||
entry cited under Considered options below; that pointer now reads `docs/spec/architecture.md`'s
|
||||
plugin scope table, which carries the repo-agnostic-versus-marketplace-specific argument.
|
||||
|
||||
## Considered options
|
||||
|
||||
@@ -66,7 +70,7 @@ maintenance in place unchanged.
|
||||
|
||||
**New standalone `plugins/apm/` plugin (rejected).** `plugins/lint/` was split out of `kyberforge`
|
||||
specifically because Vale tooling is generic and repo-agnostic, not holocron-marketplace-specific
|
||||
(see `CONTEXT.md`'s "lint plugin" entry) — the same argument applies to a generic `apm` CLI
|
||||
(see `docs/spec/architecture.md`'s plugin scope table) — the same argument applies to a generic `apm` CLI
|
||||
wrapper. The shipped `apm-install`/`apm-workflow` skills are, in fact, generic, repo-agnostic APM
|
||||
CLI documentation with no holocron-specific content, so a standalone `plugins/apm/` would have
|
||||
been a defensible split on artifact content alone. Rejected anyway, in favor of `kyberforge`,
|
||||
|
||||
@@ -329,7 +329,9 @@ mirror does carry, and is reported.
|
||||
stands; this ADR fixes the second, previously-unverified half.
|
||||
- `CONTEXT.md`'s "Plugin" and "Plugin marketplace" glossary entries are updated to describe the
|
||||
flat mirror as a second compiled-output category, alongside the existing
|
||||
`.claude-plugin/plugin.json`/`marketplace.json` description.
|
||||
`.claude-plugin/plugin.json`/`marketplace.json` description. Superseded 2026-08-17: CONTEXT.md was
|
||||
cut back to one-line definitions and no longer describes either compiled-output category;
|
||||
`docs/spec/architecture.md` is where the mirror is documented.
|
||||
- A future apm release that ships a native `.apm/`-aware plugin.json compiler (closing this gap
|
||||
upstream) would let `sync-plugin-content.sh` and its drift gate be deleted outright — nothing in
|
||||
this ADR's decision depends on the flat mirror existing beyond satisfying the current installer's
|
||||
|
||||
@@ -59,7 +59,9 @@ answers to `git-commits` and `kyberforge:skill-audit` to `skill-audit`. This is
|
||||
a project skill has no plugin to prefix. `AGENTS.md` and `CONTEXT.md` are updated to name the bare
|
||||
form, which is what apm deploys and the only form a repo consuming holocron through apm gets.
|
||||
|
||||
**Correction (2026-08-14): the namespaced form did not stop resolving.** An earlier revision of
|
||||
**Correction (2026-08-14): the namespaced form did not stop resolving.** *Superseded by the
|
||||
2026-08-17 correction below: the machine state this cites is no longer present. Both are kept
|
||||
because the pair is the finding — read neither as current.* An earlier revision of
|
||||
this consequence said every `<plugin>:<skill>` reference "was stale the moment the switch landed",
|
||||
and `AGENTS.md`/`CONTEXT.md` were written to match. That contradicts the "User scope is untouched,
|
||||
deliberately" consequence below, and the contradiction resolves against it: `~/.claude.json` still
|
||||
@@ -72,6 +74,20 @@ survives those user-scope installs eventually being converted, and the namespace
|
||||
resolves for anyone installing holocron natively, so skill bodies written for both audiences
|
||||
should name the bare skill.
|
||||
|
||||
**Correction (2026-08-17): the evidence under the correction above is gone, and the claim goes with
|
||||
it — not to its opposite.** Observed on this machine: `~/.claude/plugins/installed_plugins.json` is
|
||||
`{"version": 2, "plugins": {}}`; there is no `enabledPlugins` key anywhere in `~/.claude.json`
|
||||
(`grep -c enabledPlugins` returns 0); `~/.apm/marketplaces.json` is `{"marketplaces": []}`. The
|
||||
`holocron` entry in `~/.claude/plugins/known_marketplaces.json` survives, but a registered
|
||||
marketplace is not an installed plugin. So the user-scope installs the 2026-08-14 correction cited
|
||||
are not there, and neither is the state the *original* consequence described before it. The claim
|
||||
about the namespaced form has now been written twice off two different observations of the same
|
||||
machine, and this ADR has already reversed itself once on it. That is the finding: the fact is
|
||||
machine state, not a property of this decision, and it changes without any commit. No instruction
|
||||
file — `AGENTS.md`, `CONTEXT.md`, or a skill body — should assert either way whether
|
||||
`<plugin>:<skill>` resolves. The rule that survives every observation is the one that was always the
|
||||
actionable half: write the bare name, because it is the only form `apm install` produces.
|
||||
|
||||
**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
|
||||
@@ -117,10 +133,11 @@ pinned `resolved_commit` in `apm.lock.yaml` and does not re-resolve refs (`apm i
|
||||
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.
|
||||
**User scope is untouched, deliberately.** This decision changed project scope only; whatever is
|
||||
natively installed at user scope was left alone, and converting it is a separate decision with a
|
||||
blast radius beyond this repo. The specific inventory this paragraph used to name
|
||||
(`bin@holocron`, `gitea@holocron`, a stale `hello-world@holocron`) is machine state and is stale —
|
||||
see the 2026-08-17 correction above. The decision recorded here is unaffected by what that state is.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Every installed skill's `name` and `description` sits in every agent's context from the first token
|
||||
of every session, whether or not the skill is ever invoked. Across this repo's 39 skills that is
|
||||
23,612 characters — roughly 6,200 tokens — and the authoring rules that produced it optimised for
|
||||
23,427 characters — roughly 5,900 tokens — and the authoring rules that produced it optimised for
|
||||
triggering reliability with no counter-pressure on size. This ADR sets the budget, the shape, and the
|
||||
gates that hold them.
|
||||
|
||||
@@ -10,14 +10,32 @@ gates that hold them.
|
||||
|
||||
## Context
|
||||
|
||||
Measured before any change:
|
||||
Every `file:line` citation in this ADR is against the base commit the decision was taken on,
|
||||
`f9b919d7e3bd5e6b51fbdf88b32ace0438b313e0`, not against current `HEAD`. The change that carries this
|
||||
ADR rewrites several of the cited files, so a citation resolved against the worktree will land on
|
||||
unrelated text. Use `git show f9b919d:<path>` to follow one.
|
||||
|
||||
Measured before any change, at that commit. Method, so the figures are reproducible: sum
|
||||
`len(name) + len(description)` over the frontmatter of every `plugins/*/.apm/skills/*/SKILL.md`,
|
||||
folding `>` block scalars to the value the host actually loads (most descriptions here are folded
|
||||
scalars, so counting raw lines measures indentation instead); tokens at the standard
|
||||
~4-characters-per-token approximation `scripts/skill-size-check.sh` uses. Word counts are
|
||||
whitespace-separated tokens, and are stated as **body-only** or **whole-file** every time, never bare.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 39 skill `name` + `description` | 23,612 chars, ~6,200 tokens, **preloaded every session** |
|
||||
| 4 agent `name` + `description` | 1,325 chars, ~350 tokens, preloaded every session |
|
||||
| skill bodies | median 684 words, mean 815, p90 1,349 |
|
||||
| `MAX_WORDS` gate (`skill-audit/scripts/validate.sh:147`) | **2,770** — 2× p90 |
|
||||
| 39 skill `name` + `description` | 23,427 chars, ~5,900 tokens, **preloaded every session** |
|
||||
| 4 agent `name` + `description` | 1,325 chars, ~330 tokens, preloaded every session |
|
||||
| skill bodies (body-only words) | median 684, mean 815, p90 1,349 |
|
||||
| skill files (whole-file words) | median 816, mean 927, p90 1,526 |
|
||||
| `MAX_WORDS` gate (`skill-audit/scripts/validate.sh:147`) | **2,770** whole-file — a density proxy, not a percentile |
|
||||
|
||||
That last row is worth stating plainly, because it is the first thing this ADR is about. 2,770 is not
|
||||
derived from the corpus distribution at all: per the derivation comment in
|
||||
`scripts/skill-size-check.sh`, it is 2,770 words at the densest observed 7.22 chars/word ≈ 20,000
|
||||
chars ≈ the agentskills.io ~5,000-token ceiling. Neither percentile reaches it — 2× the body-only p90
|
||||
is 2,698 and 2× the whole-file p90 is 3,052 — and reading it as "2× p90" would pair a whole-file gate
|
||||
against a body-only distribution, which is exactly the conflation this ADR exists to stop.
|
||||
|
||||
Three findings drove this, none of which is "the descriptions drifted".
|
||||
|
||||
@@ -32,7 +50,8 @@ with six capability clusters. Across the twelve longest descriptions, 30.7% is c
|
||||
enumeration and 11.6% is composition or implementation detail that cannot affect a routing decision.
|
||||
|
||||
**Capability enumeration in a description is a correctness hazard, not only a token cost.**
|
||||
`docs/research/examples/skill-write/writing-skills/SKILL.md:154-158` reports a measured failure: "when
|
||||
`plugins/kyberforge/docs/research/examples/skill-write/writing-skills/SKILL.md:154-158` reports a
|
||||
measured failure: "when
|
||||
a description summarizes the skill's workflow, an agent may follow the description instead of reading
|
||||
the full skill content. A description saying 'code review between tasks' caused an agent to do ONE
|
||||
review, even though the skill's flowchart clearly showed TWO reviews." `git-commits` is exactly that
|
||||
@@ -41,21 +60,25 @@ chars, lowercase subject, no trailing periods, 11 standard types`) an agent can
|
||||
loading the body.
|
||||
|
||||
**The upstream sources cannot settle this.** The four skill-writing references under
|
||||
`docs/research/examples/skill-write/` disagree on what a description contains — when-only
|
||||
(`writing-skills/SKILL.md:99`), what-and-when (`skill-creator/SKILL.md:67`,
|
||||
`anthropic-best-practices.md:187`), triggers-only (`writing-great-skills/SKILL.md:28`), and
|
||||
what-plus-when-plus-negative (`write-skill/SKILL-TEMPLATE.md:5-6`). `writing-skills` and the Anthropic
|
||||
document it bundles contradict each other inside one skill directory. They also disagree on whether
|
||||
`plugins/kyberforge/docs/research/examples/skill-write/` disagree on what a description contains —
|
||||
when-only (`writing-skills/SKILL.md:99`), what-and-when (`skill-creator/SKILL.md:67`,
|
||||
`writing-skills/anthropic-best-practices.md:187`), triggers-only
|
||||
(`writing-great-skills/SKILL.md:28`), and what-plus-when-plus-negative
|
||||
(`write-skill/SKILL-TEMPLATE.md:5-6`). Those four paths are relative to that directory.
|
||||
`writing-skills` and the Anthropic document it bundles contradict each other inside one skill
|
||||
directory. They also disagree on whether
|
||||
500 lines is binding, on the inline-versus-bundle threshold, and on the TOC threshold (>100 lines vs
|
||||
>300 lines). "Grounded in the research" is therefore not available as a tiebreaker; a house choice is
|
||||
required and this is it.
|
||||
|
||||
A fourth observation shaped the body half. The best progressive-disclosure ratio in the repo belongs
|
||||
to `apm-workflow` — a 554-word body dispatching to 3,006 words of references — and the worst two
|
||||
belong to the skills that define the house standard: `skill-author` (2,760 body / 1,247 references)
|
||||
and `agent-author` (2,758 / 1,664). Both sit within twelve words of the 2,770 gate their own plugin
|
||||
enforces. A ceiling that nothing approaches is not a constraint; a ceiling that two files have grown
|
||||
into is a target.
|
||||
to `apm-workflow` — a 421-word body dispatching to 3,006 words of references — and the worst two
|
||||
belong to the skills that define the house standard: `skill-author` (2,623-word body / 1,247 words of
|
||||
references) and `agent-author` (2,582 / 1,664). Measured the other way, whole-file, those two are
|
||||
2,760 and 2,758 words — ten and twelve words under the 2,770 gate their own plugin enforces. A
|
||||
ceiling that nothing approaches is not a constraint; a ceiling that two files have grown into is a
|
||||
target. The two numbers for one file are the point: 2,623 and 2,760 describe the same `skill-author`,
|
||||
and only one of them is what either gate measures.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -69,8 +92,61 @@ clause**, and a **boundary clause**. Capability enumeration, output-format detai
|
||||
- **250 characters SUGGESTION, 400 FAIL.** The agentskills.io 1,024-character limit remains as an
|
||||
unchanged spec backstop. The SUGGESTION tier is what moves the average; the FAIL tier only stops
|
||||
outliers.
|
||||
- **A missing, valueless or `null` `description:` is a hard FAIL** in all three validators. That
|
||||
reads as a trivial precondition and is not: a `description:` line 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 below it then early-returned on the genuinely empty folded
|
||||
value — exit 0, zero output, on a blocking pre-push gate. Presence is decided on the YAML-folded
|
||||
value and nowhere else. The field this contract is entirely about is the one field a gate must
|
||||
never fail to notice is absent.
|
||||
- **Boundary clauses compress** to `Not <thing> → <skill-name>.` and must name a target that
|
||||
resolves to a real skill under `plugins/*/.apm/skills/`. This is checked deterministically.
|
||||
resolves to a real skill or agent. Resolution walks up **from the file being checked** to an
|
||||
*authoring root* — the nearest ancestor holding `plugins/*/.apm/skills` or `plugins/*/.apm/agents`,
|
||||
falling back to the nearest ancestor holding `.git`. Two passes rather than one interleaved walk,
|
||||
so a nested `.git` (a submodule, a sub-package worktree) cannot beat a real monorepo root further
|
||||
up. When an authoring root is found the universe is every skill and agent under
|
||||
`<root>/plugins/*/`, plus the target's own apm package and the packages that package declares in
|
||||
its own `apm.yml` `dependencies.apm`. Sibling plugins resolve against each other, which is what a
|
||||
monorepo means. Deployed `.claude/`/`.agents/` trees are consulted **only** when the walk found no
|
||||
plugin monorepo root — whether it landed on a bare `.git` ancestor or on nothing at all. That is
|
||||
the consumer case, where there is no monorepo to read. The condition is which of the two passes
|
||||
matched, never a name-count delta: a single-plugin monorepo re-collects its own package and adds
|
||||
no new name, so a delta test reads zero there and would pull the deployed trees back in. What the
|
||||
resolver must never do is
|
||||
derive the universe from its own location: a `${BASH_SOURCE}`-relative repo root leaked this repo's
|
||||
39-skill universe into every consumer repo running the hook through pre-commit, so a consumer skill
|
||||
routing to `skill-audit` resolved against a plugin it had never installed. Checked
|
||||
deterministically. A description carrying **no** boundary clause at all is a SUGGESTION, for skills
|
||||
and agents alike: most descriptions want one, some genuinely have no near-miss sibling to exclude,
|
||||
and that judgment is not a script's to make.
|
||||
- **The verdict must not depend on whether `apm install` has been run.** Deployed trees are
|
||||
gitignored install output, present only on a machine that has run it. Four cross-plugin targets
|
||||
here (`gitea-branches` → `git-branches`, `gitea-branches` → `git-history`, `gitea-issues` →
|
||||
`git-branches`, `gitea-workflow` → `git-workflow`) once resolved through `.claude/skills/` alone,
|
||||
so the same commit measured 2 dangling targets on a developer machine and 6 on a fresh clone. A
|
||||
gate shipping hot with no baseline cannot give two answers. Under the walk-up those four resolve
|
||||
because sibling plugins are in the universe — no plugin here declares a cross-plugin apm
|
||||
dependency, and none needs to. Verified: a tree holding only `plugins/` and the root `apm.yml`,
|
||||
with no `.claude/` or `.agents/` anywhere, now produces findings identical to the working tree —
|
||||
26 description FAILs, 9 body FAILs, 2 dangling targets, 0 missing references, 58 SUGGESTIONs.
|
||||
- **The universe is the apm marketplace, and nothing else.** A routing target resolves to a skill or
|
||||
an agent, or it does not resolve. Host built-ins are deliberately outside it: `/compact`, `/clear`
|
||||
and `/init` are Claude Code slash commands with no counterpart in Copilot CLI or Codex, so a
|
||||
vendor-neutral `.apm/` description routing to one is a portability defect and the hard FAIL is a
|
||||
true positive, not a false one. An allowlist of known built-ins was **rejected**: it answers a
|
||||
different question ("does this exist on *some* host?"), it cannot answer that portably from a
|
||||
single source file, and it goes stale the next time a host ships a command — reintroducing the
|
||||
same-commit-two-verdicts failure the bullet above exists to close. An author who needs to mention
|
||||
one writes it un-slashed (``the `compact` built-in``), which is not route notation and makes no
|
||||
routing claim.
|
||||
- **Blocking is scoped to a sentence, which makes sentence boundaries load-bearing.** A prose-form
|
||||
target earns a hard error only when its own sentence names another target that *resolves*; route
|
||||
notation (`/name`, `→ name`) is exempt and always blocks. So the splitter is part of the contract,
|
||||
not a detail of it. `e.g. "…"` is not a sentence end, and a sentence opening with a code span or a
|
||||
lowercase skill name is a start; getting either wrong moves targets between the two tiers in
|
||||
opposite directions — a stranded corroborator silently demotes a real finding to SUGGESTION, and a
|
||||
missed boundary lets one sentence vouch for a target it never stood beside, producing a hard FAIL
|
||||
with no escape hatch.
|
||||
- **The blanket pushiness rules are deleted.** `skill-author/SKILL.md:104` and
|
||||
`description-quality.md:21` are replaced by a conditional: add an indirect trigger only where the
|
||||
user's natural phrasing genuinely omits the domain word — true for the `gitea-*` family, false for
|
||||
@@ -89,11 +165,16 @@ prose move to `references/` behind an explicit "read X when Y" trigger.
|
||||
current state.
|
||||
- **Dispatch is mandatory at two or more mutually exclusive flows.** The body carries the dispatch
|
||||
table and the gates that apply to every branch; each flow lives in its own self-contained
|
||||
`references/` file. This is `apm-workflow/SKILL.md:33-41` promoted from accident to rule.
|
||||
`references/` file. This is `apm-workflow/SKILL.md:33-41` promoted from accident to rule. "Two
|
||||
mutually exclusive flows" is not decidable from file text, so this rule is auditor judgment — see
|
||||
Enforcement below for what that means and does not mean.
|
||||
- **Every `references/<file>.md` a body names must exist.** A dispatch table pointing at a file that
|
||||
was never written is a silently dead branch. Checked deterministically.
|
||||
- **Gotchas are constrained.** A Gotcha must state a fact that contradicts a reasonable default —
|
||||
something the agent gets wrong by acting sensibly. Maximum five entries. A Gotcha that paraphrases
|
||||
a step in the body below it is a FAIL. A Gotchas section exceeding 25% of the body is a
|
||||
SUGGESTION.
|
||||
something the agent gets wrong by acting sensibly. More than five entries is a SUGGESTION, as is a
|
||||
Gotchas section exceeding 25% of the body; both are countable and both are checked
|
||||
deterministically. A Gotcha that paraphrases a step in the body below it is a FAIL, but a FAIL an
|
||||
auditor issues, not a script — semantic equivalence is not pattern-matchable.
|
||||
|
||||
### Agents
|
||||
|
||||
@@ -101,6 +182,14 @@ Agents take the same description gates — they are preloaded identically — an
|
||||
A skill body is loaded into the caller's context, competing with the live conversation; an agent body
|
||||
becomes the system prompt of a fresh context. The rationale for the 900-word FAIL does not transfer.
|
||||
|
||||
That exemption is expressed in `agent-audit/scripts/validate.sh`, which has no body constant, and in
|
||||
the `files:` pattern of the `skill-size-check` pre-commit hook, which is `SKILL.md`-only. It is *not*
|
||||
expressed in `scripts/skill-size-check.sh` itself, which measures whatever path it is handed —
|
||||
running it directly over `plugins/*/.apm/agents/*.agent.md` today reports 900-word body FAILs on
|
||||
`git-orchestrate` (933), `gitea-orchestrate` (1,199) and `apm-orchestrate` (1,080). Agents escape by
|
||||
file pattern, not by the script knowing the difference. Anyone widening that pattern to cover agents
|
||||
would silently enforce a gate this ADR declines to set.
|
||||
|
||||
A plugin-scope agent is a single file with no sibling `references/` directory, so it cannot disclose
|
||||
to itself — it can only delegate to skills. `agent-audit` therefore gains a **delegation check**: an
|
||||
agent body that restates a procedure owned by a skill it can invoke is a FAIL, with the fix being
|
||||
@@ -126,26 +215,84 @@ type of input they take should be **one skill with a dispatch table**. This catc
|
||||
one-or-two-file agent pair, per ADR-0005 and ADR-0016) and their overlap is in the improve flow
|
||||
rather than the core job.
|
||||
|
||||
**DEFERRED — not implemented in the change that carries this ADR. Tracked as issue #101.** Both
|
||||
skills still exist separately, and this change made the split deeper rather than shallower: retrofit
|
||||
to the dispatch pattern took `skill-audit` from 3 reference files to 7 and `agent-audit` from 4 to 8,
|
||||
and their two same-named `references/description-quality.md` files now differ on 100 of ~120 lines
|
||||
after normalising `skill`/`agent`, where before they were closer. The merge stays the decision; it
|
||||
reopens ADR-0008 (agent-audit's single-file invocation contract) and touches every call site in
|
||||
`skill-author`, `agent-author` and `forge`, which is why it is its own change and not a rider on
|
||||
this one. Recorded here rather than dropped, so the gap between the rule and the tree is deliberate
|
||||
and dated instead of discovered later.
|
||||
|
||||
### Enforcement and rollout
|
||||
|
||||
Gates land where the existing gates already live — no new layer:
|
||||
Gates land where the existing gates already live — no new layer. The table below is exhaustive about
|
||||
which tier each rule is in, because the failure this ADR is most exposed to is a rule filed under
|
||||
"Enforcement" that no validator implements:
|
||||
|
||||
| Check | Home |
|
||||
|---|---|
|
||||
| description and body counts, resolvable boundary targets | `skill-audit/scripts/validate.sh`, `scripts/skill-size-check.sh` |
|
||||
| prose patterns (composition-note openers, restatement) | `plugins/kyberforge/.apm/skills/*/assets/vale/styles/Kyberforge/` |
|
||||
| judgment calls | `references/description-quality.md`, `references/body-discipline.md` |
|
||||
| Check | Applies to | Tier | Home |
|
||||
|---|---|---|---|
|
||||
| description characters (250 SUGGESTION / 400 FAIL) | skills, agents | deterministic | `scripts/skill-size-check.sh`; constants mirrored in `skill-audit/scripts/validate.sh` and `agent-audit/scripts/validate.sh` |
|
||||
| body-only words (600 SUGGESTION / 900 FAIL) | skills | deterministic | `skill-size-check.sh`, `skill-audit/scripts/validate.sh` |
|
||||
| description present and non-empty (ERROR) | skills, agents | deterministic | same |
|
||||
| boundary target resolves to a real skill or agent (ERROR when written as `/name` or `-> name`, or when its own sentence names another target that resolves; SUGGESTION otherwise) | skills, agents | deterministic | same |
|
||||
| boundary clause absent (SUGGESTION) | skills, agents | deterministic | same |
|
||||
| Gotchas entry count over five (SUGGESTION) | skills | deterministic | same |
|
||||
| Gotchas over 25% of the body (SUGGESTION) | skills | deterministic | same |
|
||||
| every `references/<file>.md` a body names exists (ERROR) | skills | deterministic | same |
|
||||
| description opener, composition notes in a description | skills, agents | prose pattern | `plugins/kyberforge/.apm/skills/*/assets/vale/styles/Kyberforge/` |
|
||||
| a Gotcha paraphrasing a body step | skills | **auditor judgment** | `references/body-discipline.md` |
|
||||
| dispatch at two or more mutually exclusive flows | skills | **auditor judgment** | `references/body-discipline.md` |
|
||||
| delegation: an agent body restating a skill's procedure | agents | **auditor judgment** | `agent-audit` |
|
||||
| capability enumeration, restatement, trigger quality | skills, agents | **auditor judgment** | `references/description-quality.md` |
|
||||
|
||||
**Blocking immediately, with no baseline file.**
|
||||
The rows in bold are stated as FAILs in the Decision above and are FAILs an *auditor* issues. None of
|
||||
them is countable: "does this Gotcha paraphrase step 4", "are these two flows mutually exclusive" and
|
||||
"does this agent body restate what `git-commits` already owns" are semantic questions, and a script
|
||||
that guessed at them would be a worse gate than no gate, because it would be believed. They are not
|
||||
enforced, they are reviewed, and this table exists so that distinction is written down rather than
|
||||
inferred from whether a validator happens to have been written yet.
|
||||
|
||||
Two of the deterministic rows are tuned for **false positives over recall**, and what they decline to
|
||||
see is part of the contract. On target extraction: a bare hyphenated name counts only inside a
|
||||
boundary sentence, and a single-word name is never matchable bare — `research`, `triage`, `forge`,
|
||||
`prototype` and `tdd` are all real skill names *and* ordinary English, so it must be written
|
||||
`` `forge` `` or `/forge` to be seen at all. Grammar then decides whether a recognised target may
|
||||
raise an error: one followed by an ordinary lowercase noun is a compound **modifier**, not a route
|
||||
("use pre-commit hooks instead of ad-hoc scripts", "invoke the pull-request template"), so it is
|
||||
confirm-only — it still resolves and still counts as a route when the name exists, but it can never
|
||||
dangle. Only a *terminal* target can. The compressed arrow form `→ <name>` is exempt from that
|
||||
follower test and is always error-eligible, because nothing reads as a compound modifier after an
|
||||
arrow; a `/slash` target reached through a route verb is **not** exempt and takes the same test. The
|
||||
simpler rule — "only marked targets may dangle" — was available and would have been wrong here: both
|
||||
live true positives are bare, `research`'s "(use neuledge-context)" and the `gitea-labels-` /
|
||||
`milestones` fold. On the body-shape checks: a `## Gotchas` heading must *end* in "gotchas", not
|
||||
merely contain the word, so `## Gotcha handling` and `## Why gotchas matter` are prose sections and
|
||||
are skipped; fenced code blocks are masked out of heading detection and entry counting, so a fenced
|
||||
example list is not mistaken for the section; and a `references/` pointer named on a line
|
||||
that also says the file is gone ("removed", "deprecated", "no longer") is read as a historical
|
||||
mention rather than a dead dispatch entry. Note the 25% fraction is deliberately *not* fence-masked
|
||||
on either side — fenced lines are real body words, and the fraction is measured against the whole
|
||||
body.
|
||||
|
||||
**The deterministic tier blocks immediately, with no baseline file.**
|
||||
|
||||
Three pre-existing contradictions are fixed in the same change, because they are the contract:
|
||||
|
||||
- `skill-audit/SKILL.md:58` asks whether the description opens with an action verb ("Audits…",
|
||||
"Reviews…") while `:101` and `DescriptionOpener.yml` require an imperative "Use when…" opener. The
|
||||
criterion is unsatisfiable against the house's own skills, both of which open with "Use when".
|
||||
- `DescriptionOpener.yml`'s regex is anchored to `^This (skill|agent)\b`, so `gitea-workflow` ("This
|
||||
is the human-facing entry point…") and `gitea-labels-milestones` ("This is a cross-cutting shared
|
||||
skill…") both violate the rule and pass the linter.
|
||||
"Reviews…"), while `:56` defers the same question to `Kyberforge.DescriptionOpener` and
|
||||
`skill-author/SKILL.md:101` requires an imperative "Use when…" opener. The criterion is
|
||||
unsatisfiable against the house's own skills, both of which open with "Use when".
|
||||
- `DescriptionOpener.yml` is anchored to `^This (skill|agent)\b`, which misses a plain `This …`
|
||||
opener; it is widened here to `^This\b`. The anchor itself stays. Composition prose that sits
|
||||
*mid*-description — `gitea-workflow`'s "This is the human-facing entry point…" at character 377,
|
||||
`gitea-labels-milestones`'s "This is a cross-cutting shared skill…" at character 300 — was never in
|
||||
the opener rule's scope and correctly is not: under `scope: text.frontmatter.description` the `^`
|
||||
anchors to the start of the whole folded value, and un-anchoring to reach mid-description text was
|
||||
measured at 5 hits and 5 false positives and rejected (`LESSONS.md`, 2026-08-14). The real gap is
|
||||
that no rule covered that text at all, which a new token-list rule, `Kyberforge.CompositionNote`,
|
||||
closes: 10 alerts across four `gitea-*` skills, 0 false positives.
|
||||
- `description-quality.md:45-50` has no FAIL condition for internal-mechanics content, which is why
|
||||
`skill-author/SKILL.md:102` never bit.
|
||||
|
||||
@@ -159,24 +306,35 @@ retrofits kyberforge's own four author/audit skills, so the figures on landing a
|
||||
With the gate hot and no baseline, a one-line
|
||||
fix to `gitea-prs` cannot be committed until that skill meets the contract. This is deliberate — it
|
||||
guarantees convergence and avoids a half-state — but it means the retrofit is lazy and *mandatory*
|
||||
rather than deferred. The follow-up retrofit issue should be prioritised accordingly, and the risk it
|
||||
rather than deferred. Issue #99 tracks it and should be prioritised accordingly, and the risk it
|
||||
carries is the ordinary one for hot gates: a gate expensive enough to be inconvenient gets bypassed
|
||||
with `SKIP=` and loses its authority.
|
||||
|
||||
**A second hot gate ships alongside it, and it is easy to miss.** `Kyberforge.CompositionNote` is
|
||||
`level: error` like every other rule in that style, so `pre-commit run --all-files` is red on 10
|
||||
alerts across `gitea-issues`, `gitea-labels-milestones`, `gitea-prs` and `gitea-workflow`
|
||||
independently of anything `skill-size-check` reports. Someone scoping the #99 retrofit off the size
|
||||
findings alone will fix those and still be blocked. The two gates want fixing together.
|
||||
|
||||
**A ceiling does not produce an average.** If every author writes to the 400-character FAIL, the
|
||||
preload lands at 15,600 chars — a 34% cut, not the ~50% intended. The halving depends entirely on the
|
||||
preload lands at 39 × 400 = 15,600 chars — a 33% cut off 23,427, not the ~50% intended. Writing to
|
||||
the 250-character SUGGESTION instead lands at 9,750, a 58% cut. The halving depends entirely on the
|
||||
250-character SUGGESTION tier being visible and respected. That tier works here in a way it does not
|
||||
elsewhere in this repo: `skill-audit` already reports `PASS (N suggestions)` as a first-class
|
||||
outcome. This is explicitly **not** the failure ADR-0013 records — Vale warnings are invisible
|
||||
because vale's exit code keys on `error` alone, but these gates live in `validate.sh` and
|
||||
`skill-audit`, where a SUGGESTION reaches the report. Realistic landing is 34-55% down, not a
|
||||
guaranteed 50%.
|
||||
`skill-audit`, where a SUGGESTION reaches the report. Realistic landing is somewhere in that 33-58%
|
||||
band, not a guaranteed 50%.
|
||||
|
||||
**A word gate cannot detect the defect it is standing in for.** `git-commits` carries thirteen
|
||||
Gotchas of which four restate steps in its own Workflow (`:32` ≡ step 9, `:33` ≡ step 9, `:36` ≡ step
|
||||
2, `:31` ≡ the description) — 1,217 words that pass any plausible gate. The counts are a backstop to
|
||||
the dispatch rule and the Gotchas constraint, not a substitute for them, and should not be read as
|
||||
the mechanism.
|
||||
**A word gate cannot detect the defect it is standing in for.** `git-commits` carries twelve Gotchas
|
||||
of which four restate steps in its own Workflow (`:32` ≡ step 9, `:33` ≡ step 9, `:36` ≡ step 2,
|
||||
`:31` ≡ the description). Its body is 1,102 words and its whole file 1,217, so it does fail the
|
||||
900-word body FAIL — but for its length, not for the restatement. The four duplicated Gotchas are 114
|
||||
words between them; delete every one and the file still fails, while a skill 250 words shorter with
|
||||
the identical defect passes clean. The two properties are uncorrelated, which is why the counts are a
|
||||
backstop to the dispatch rule and the Gotchas constraint — both of which are auditor judgment for the
|
||||
semantic half, per the Enforcement table — and not a substitute for them. Reading the word gate as
|
||||
the mechanism is the specific mistake this paragraph exists to prevent.
|
||||
|
||||
**Some skills legitimately need more description budget than others.** A tiered limit keyed to
|
||||
sibling density was considered and rejected as too clever; the flat 250/400 pair means the `gitea-*`
|
||||
@@ -184,22 +342,34 @@ and `git-*` families — where every sibling shares a keyword and boundary claus
|
||||
— are the ones most likely to sit at the FAIL tier permanently. If the retrofit shows that family
|
||||
routing degrades, the tier is the first thing to revisit.
|
||||
|
||||
**Four broken routing targets are live and are not fixed here.** `skill-audit` routes to
|
||||
`/skill-improve` twice in its description plus `README.md:10`, and no such skill exists — the real
|
||||
target is `skill-author`. `research` routes to `neuledge-context`, which exists only inside that
|
||||
string. `agent-author` says "Do not use for read-only review — examine agent files manually",
|
||||
routing away from `agent-audit`, the correct sibling. `gitea-issues` contains the literal string
|
||||
`gitea-labels- milestones`, a stray space introduced by YAML folding, breaking the skill name in
|
||||
preloaded text. The resolvable-target check added here will fail on all four the moment those files
|
||||
are touched; fixing them is split into its own issue.
|
||||
**Four broken routing targets were found; two are fixed here and two are live.** Tracked as issue
|
||||
#100.
|
||||
|
||||
- `skill-audit` routed to `/skill-improve` twice in its description plus `README.md:10`, and no such
|
||||
skill exists — the real target is `skill-author`. **Fixed here**, as a side effect of retrofitting
|
||||
kyberforge's own skills.
|
||||
- `agent-author` said "Do not use for read-only review — examine agent files manually", routing away
|
||||
from `agent-audit`, the correct sibling. **Fixed here**, same way. Note this one was never
|
||||
detectable by the resolvable-target check and never will be: "examine agent files manually" names
|
||||
no target, and a check that resolves names cannot see a name that is absent. A misroute to nowhere
|
||||
is a review finding, not a gate finding.
|
||||
- `research` routes to `neuledge-context`, which exists only inside that string. **Live.**
|
||||
- `gitea-issues` carries the literal string `gitea-labels- milestones` in its folded description, a
|
||||
stray space introduced by YAML wrapping mid-token, breaking the skill name in preloaded text.
|
||||
**Live** — the check reports it as a dangling `gitea-labels`.
|
||||
|
||||
So the check fires on 3 of the 4 against the base commit and on 2 at the tip of this change, and
|
||||
`tests/test-skill-size-check.sh` probes exactly those three by name rather than asserting a count, so
|
||||
it degrades to SKIP as #100 lands rather than going stale.
|
||||
|
||||
**Duplication between `skill-author` and `agent-author` survives un-gated.** The merge rule
|
||||
deliberately excludes the author pair, so the commit-verification argument in four near-copies, the
|
||||
root-cause grouping rule in four copies, and the wholesale clone of the "Improving an existing X"
|
||||
flow all remain. Cache isolation makes them structurally unavoidable
|
||||
(`skill-audit/SKILL.md:95` forbids cross-skill references; `LESSONS.md:107` records why), so the
|
||||
options are a sync gate or continued drift. This is an input to the kyberforge-bodies follow-up
|
||||
issue, not a solved problem.
|
||||
options are a sync gate or continued drift. This is an input to issue #101, which carries both halves
|
||||
of the kyberforge duplication problem — the deferred audit-pair merge and this — not a solved
|
||||
problem.
|
||||
|
||||
**Provenance frontmatter is explicitly out of scope.** `LESSONS.md:63` asserts that non-routing
|
||||
frontmatter (`source_keys`, `category`, `version`) is loaded at agent startup, which would make the
|
||||
@@ -211,11 +381,14 @@ the ADR-0009 provenance machinery for no runtime gain. The metadata was added de
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
Upstream citations below are relative to
|
||||
`plugins/kyberforge/docs/research/examples/skill-write/`, as in Context above.
|
||||
|
||||
- **Keep pushiness, raise the budget to ~500 chars.** Undertriggering is the worse failure mode — a
|
||||
skill that never fires is worth nothing regardless of cost — and `skill-creator/SKILL.md:67`
|
||||
explicitly recommends being "pushy" against an observed undertriggering tendency. Rejected because
|
||||
that claim is an unmeasured assertion about an older model, and because the correctness hazard in
|
||||
`writing-skills:154-158` cuts the other way: a fat description is not merely expensive, it is a
|
||||
`writing-skills/SKILL.md:154-158` cuts the other way: a fat description is not merely expensive, it is a
|
||||
shortcut agents take instead of reading the body. Would have landed a 35% cut.
|
||||
- **A trigger-eval loop to set lengths empirically.** `skill-creator/SKILL.md:337-404` specifies 20
|
||||
queries per skill, 8-10 positive and 8-10 near-miss, with a 60/40 train/test split selecting on
|
||||
@@ -226,8 +399,9 @@ the ADR-0009 provenance machinery for no runtime gain. The metadata was added de
|
||||
skill's edit fail on account of another skill's growth, and because it is meaningless for an
|
||||
external consumer installing a subset of the plugins.
|
||||
- **500-word body FAIL, matching `writing-skills/SKILL.md:217-221`.** Best-grounded in upstream and
|
||||
would align this repo with the tightest source. Rejected because it fails 30 of 39 skills, and a
|
||||
blunt gate gets satisfied by deleting content rather than relocating it.
|
||||
would align this repo with the tightest source. Rejected because it fails 28 of 39 skills body-only
|
||||
(35 of 39 measured whole-file), and a blunt gate gets satisfied by deleting content rather than
|
||||
relocating it.
|
||||
- **A shrinking baseline file** recording each non-compliant skill's current numbers, failing only on
|
||||
growth. Would have made the retrofit a visible burn-down instead of a wall. Rejected in favour of
|
||||
hot gates.
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
# A plugin's published description states its domain boundary and never enumerates its skills
|
||||
|
||||
Three of this repo's six plugins publish a `description` that lists the skills they ship. That style
|
||||
has now failed three times in four days, the third time inside the correction for the second. It is
|
||||
enforced by nothing, it obliges a marketplace release on every skill addition, and it was never
|
||||
applied to the other three plugins. This ADR retires it: a published description says what the
|
||||
plugin is *for*, and the inventory lives where an inventory can be read off the tree.
|
||||
|
||||
**Status: accepted (2026-08-17).**
|
||||
|
||||
## Context
|
||||
|
||||
A plugin's published description is one string authored twice — in `plugins/<name>/apm.yml` and in
|
||||
the matching `marketplace.packages[]` entry of the root `apm.yml` — and compiled into four generated
|
||||
files per plugin edit: the plugin's `.claude-plugin/plugin.json` and `.github/plugin/plugin.json`,
|
||||
plus the repo-wide `.claude-plugin/marketplace.json` and its `.github/plugin/marketplace.json`
|
||||
mirror. (`.agents/plugins/marketplace.json`, apm's codex profile, carries no per-package
|
||||
`description` or `version` at all and is unaffected.) It is the only text a consumer sees in a marketplace listing before
|
||||
installing. It is **not** a SKILL.md `description`: it is never preloaded into an agent's context and
|
||||
routes nothing at runtime. ADR-0020 governs that other artifact; this one governs this one. The
|
||||
overlap is a finding, not a scope: ADR-0020 established that capability enumeration in a description
|
||||
is "a correctness hazard, not only a token cost". The hazard at this layer is different — staleness
|
||||
in published metadata rather than an agent shortcutting the body — but the enumeration is the same
|
||||
construct and it fails the same way.
|
||||
|
||||
Measured at `de84d1b`, the branch tip before this change. Each figure is reproducible from the tree:
|
||||
skill counts are `ls plugins/<name>/.apm/skills/ | wc -l`, description text is
|
||||
`plugins/<name>/apm.yml`.
|
||||
|
||||
| Plugin | Style | Skills | Items enumerated | Skills named | Unnamed |
|
||||
|---|---|---|---|---|---|
|
||||
| `bin` | enumeration | 11 | 8 | 9 | `caveman`, `zoom-out` |
|
||||
| `git` | enumeration | 9 | 8 | 8 | `git-workflow` |
|
||||
| `gitea` | enumeration | 7 | 7 | 6 | `gitea-workflow` |
|
||||
| `core` | boundary | 3 | — | — | — |
|
||||
| `kyberforge` | boundary | 7 | — | — | — |
|
||||
| `lint` | boundary | 2 | — | — | — |
|
||||
|
||||
Three failures, in order.
|
||||
|
||||
**`bb9158d` (2026-08-14) — `core`'s description described `bin`.** The text it deleted read
|
||||
"Cross-cutting utility skills for everyday AI-assisted coding — triage, diagnosis, architecture
|
||||
review, and session navigation." All four items are real skills and not one of them is `core`'s:
|
||||
they are `bin`'s `triage`, `diagnose`, `improve-codebase-architecture` and `zoom-out`. `core` ships
|
||||
`agentsmd-author`, `agentsmd-audit` and `provider-adapter-author`, and the published description
|
||||
named none of them.
|
||||
|
||||
This is the failure the whole style was later adopted against, and it is worth being exact about
|
||||
what it was, because the record has been read the other way twice since. It was **wrong content**,
|
||||
not an incomplete list. The description was a syntactically perfect, complete, four-item enumeration
|
||||
of a real skill set; it just belonged to a different plugin. Enumerating harder could not have caught
|
||||
it, and a gate that asked "does every enumerated item exist as a skill?" would have passed it — all
|
||||
four did exist. `bb9158d`'s own fix went the other direction: it replaced the enumeration with a
|
||||
domain boundary, and `core` has needed no correction since. The precedent set by that commit was
|
||||
therefore *boundary*, and the two commits below cite it while doing the opposite.
|
||||
|
||||
**`65bac15` (2026-08-17) — `git` advertised `gitea`'s domain, `gitea` advertised a skill that does
|
||||
not exist.** `git` read "conventional commits, branch management, pull requests, and feature flow";
|
||||
pull requests reach the forge over HTTP and are `gitea`'s, which is the exact boundary
|
||||
`docs/spec/architecture.md` draws between the two plugins. `gitea` read "issues, pull requests,
|
||||
milestones, releases, and wikis"; `grep -ri wiki plugins/gitea/.apm/` returns nothing and no wiki
|
||||
skill has ever existed. Both were repaired by re-enumerating.
|
||||
|
||||
**`de84d1b` (2026-08-17) — the re-enumeration was itself incomplete.** `bin`'s "A place for things to
|
||||
be binned" was replaced with an eight-item list over eleven skills; `caveman` and `zoom-out` are
|
||||
absent. `zoom-out` is the same skill `bb9158d` had called "session navigation" three days earlier
|
||||
while deleting it from the wrong plugin's description — named when it was in the wrong place,
|
||||
unnamed once it was in the right one. And the miss is not confined to `bin`: `git-workflow` is
|
||||
unnamed in `git`'s corrected description, though `65bac15`'s own commit message states it was added
|
||||
("omitting pc-author/pc-run, git-submodules and git-workflow"), and `gitea-workflow` is unnamed in
|
||||
`gitea`'s. Across the three plugins, 23 of 27 skills are named at the third attempt.
|
||||
|
||||
**Nothing checks any of this.** `scripts/check-manifests.sh` does not contain the string
|
||||
`description`. The three ADR-0020 validators (`scripts/skill-size-check.sh` and skill-audit's and
|
||||
agent-audit's `validate.sh`) gate on SKILL.md and agent frontmatter; they do open `apm.yml`, but only
|
||||
to read `dependencies.apm` when resolving the boundary-target universe — none of them reads the
|
||||
`description:` key, and their hook globs match `SKILL.md` and `*.agent.md` only. `apm audit --ci`,
|
||||
`apm pack --check-clean` and `scripts/sync-plugin-content.sh --check --all` all compare compiled
|
||||
output against `apm.yml`, so their entire job is to propagate whatever the description says into
|
||||
those four files byte-for-byte and confirm they match. The `wiki` claim passed every one of the fourteen pre-push hooks, every day it
|
||||
was published.
|
||||
|
||||
**And the obligation is unbounded.** Under enumeration, adding one skill to `bin`, `git` or `gitea`
|
||||
means editing two copies of a prose string on top of the version bumps and regeneration any skill
|
||||
addition already owes under this repo's release policy
|
||||
(`plugins/kyberforge/.apm/skills/apm-workflow/references/marketplace.md`). The bumps are not the
|
||||
marginal cost — the prose edit is, and it is the half nothing checks. A skill *rename* triggers the
|
||||
same, for a string no consumer can tell went stale. 27 of the repo's 39 skills sat behind
|
||||
a description carrying that obligation; the other 12 did not, and their three plugins have generated
|
||||
no defect of this class.
|
||||
|
||||
### Scope
|
||||
|
||||
This decision covers the six plugins this repo authors. The root marketplace also lists
|
||||
`mattpocock-skills`, a third-party package whose description is not this repo's to write; its entry
|
||||
is out of scope and is left as published upstream.
|
||||
|
||||
## Decision
|
||||
|
||||
**A plugin's published `description` states the plugin's domain boundary. It does not enumerate the
|
||||
skills the plugin ships, by name or by paraphrase.**
|
||||
|
||||
- The boundary answers "what kind of work belongs to this plugin, and where is its edge against its
|
||||
nearest sibling" — the question a consumer deciding whether to install is actually asking. It is
|
||||
stable under skill addition, rename and removal, which is the entire point: an artifact that does
|
||||
not change when the tree changes cannot go stale against it.
|
||||
- **The boundary must cover everything the plugin actually ships.** A boundary drawn narrower than
|
||||
the contents is the same defect as an incomplete enumeration, one level up, and it is the specific
|
||||
risk in this change. `git` carries `pc-author` and `pc-run`, which are not git operations at all;
|
||||
"Skills for working with Git" silently drops them, so the boundary names the pre-commit hooks
|
||||
explicitly rather than trusting a reader to file them under Git.
|
||||
- The two copies — package `apm.yml` and the root `marketplace.packages[]` entry — stay identical.
|
||||
This is already the rule in practice and both prior corrections state why: the root entry is what
|
||||
reaches the compiled marketplace, so fixing only the package manifest leaves it half-propagated.
|
||||
- The three descriptions, rewritten here, with `core`/`kyberforge`/`lint` shown for register:
|
||||
|
||||
| Plugin | Published description | Chars |
|
||||
|---|---|---|
|
||||
| `bin` | Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin. | 152 |
|
||||
| `git` | Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it. | 146 |
|
||||
| `gitea` | Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone. | 134 |
|
||||
| `core` | *(unchanged)* Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it. | 101 |
|
||||
| `kyberforge` | *(unchanged)* Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace. | 105 |
|
||||
| `lint` | *(unchanged)* Skills and agents for configuring and running linters. | 54 |
|
||||
|
||||
- **No gate is added.** This is a deliberate omission and the reasoning is below, not an item left
|
||||
for later.
|
||||
|
||||
### Why no gate
|
||||
|
||||
The check enumeration would need — "every skill directory appears in the description" — was writable
|
||||
in principle and was never written, including by the two commits that corrected an enumeration by
|
||||
enumerating again and had every reason to. It is also only half a check: it
|
||||
catches a skill missing from the list, and it cannot catch `wiki`, because "this noun does not name
|
||||
any skill" requires a vocabulary of permissible non-skill nouns that no one is going to maintain.
|
||||
Under a boundary there is no correspondence left to check, which is the property being bought.
|
||||
|
||||
What survives un-gated is `bb9158d`'s actual failure: a boundary that is simply wrong about its
|
||||
plugin. That was never machine-checkable in either style — the text was a well-formed description of
|
||||
a real plugin — and it is caught by the same review that has to happen when a published,
|
||||
consumer-facing string is edited at all. A gate that would catch it needs a declared per-plugin
|
||||
skill-to-boundary mapping for the description to be checked against, which is a second artifact
|
||||
requiring exactly the per-skill maintenance this ADR exists to delete, relocated one file over.
|
||||
|
||||
Two cheap partial gates were considered and rejected in the same breath. Forbidding a comma-separated
|
||||
run of three or more noun phrases is a prose heuristic that fires on `lint`'s perfectly good
|
||||
"configuring and running linters" class of sentence. Forbidding any string matching a skill directory
|
||||
name under `plugins/<name>/.apm/skills/` bans legitimate boundary vocabulary — `git-branches` exists,
|
||||
and a `git` boundary has every right to say "branches". Both would be believed, and both would be
|
||||
wrong, which ADR-0020 already records as worse than no gate.
|
||||
|
||||
## Considered options
|
||||
|
||||
**Keep enumeration and gate it.** The only option that makes the current style safe. Rejected on the
|
||||
three grounds above: the check is one-directional, it cannot see an invented capability, and it makes
|
||||
a marketplace release the consequence of adding a directory. It also hard-couples published consumer
|
||||
copy to internal directory names, so a skill rename becomes a version bump on the plugin and on the
|
||||
marketplace.
|
||||
|
||||
**Enumerate consistently across all six plugins**, on the grounds that the real defect is the split
|
||||
style. Rejected: it takes an obligation that has produced three failures on three plugins and applies
|
||||
it to six. The measured outcome of the most recent attempt to enumerate carefully, with the defect
|
||||
fresh and two prior commits as precedent, is four skills unnamed.
|
||||
|
||||
**Cap the description length**, mirroring ADR-0020's 250/400-character tiers, on the theory that a
|
||||
short description has no room to enumerate. Rejected because length does not measure correspondence:
|
||||
`gitea`'s failing description was 96 characters and asserted a skill that has never existed, while
|
||||
`bin`'s 176-character enumeration is under the same cap. All six descriptions here, before and after,
|
||||
sit inside ADR-0020's tiers; the tier would have been silent through all three failures.
|
||||
|
||||
**Delete the description to a bare name.** Rejected: apm's Claude marketplace mapper emits
|
||||
`description` into `marketplace.json`, and it is the only prose a consumer sees before installing.
|
||||
|
||||
**Point the description at the plugin's `README.md`.** Rejected: a marketplace listing renders a
|
||||
string, not a link — and the README's own plugin list carries the same enumeration with the same
|
||||
staleness, so this relocates the defect rather than fixing it.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Three descriptions are rewritten and the compiled output regenerated.** Eight generated files
|
||||
change: `plugins/{bin,git,gitea}/.claude-plugin/plugin.json`,
|
||||
`plugins/{bin,git,gitea}/.github/plugin/plugin.json`, `.claude-plugin/marketplace.json` and its
|
||||
byte-identical `.github/plugin/marketplace.json` mirror. `.agents/plugins/marketplace.json` (the
|
||||
codex profile) is unchanged and correctly so — it carries no per-package `description` or `version`
|
||||
field at all, only `name`, `source`, `policy` and `category`.
|
||||
|
||||
**Version bumps, all PATCH under the `per_package` strategy:** `bin` 1.1.4 → 1.1.5, `git` 1.3.4 →
|
||||
1.3.5, `gitea` 1.3.5 → 1.3.6, `marketplace.version` 0.4.4 → 0.4.5.
|
||||
|
||||
**The root `apm.yml` top-level `version:` is restored to lockstep with `marketplace.version`,
|
||||
0.4.2 → 0.4.5.** These two fields have moved together in every commit that has ever touched root
|
||||
`apm.yml` — 0.3.2, 0.3.3, 0.3.4, 0.4.0, 0.4.1, 0.4.2 in both — until `65bac15` and
|
||||
`de84d1b` on this branch bumped `marketplace.version` to 0.4.3 and then 0.4.4 while leaving the
|
||||
top-level field at 0.4.2. Lockstep is not folklore: it is stated at
|
||||
`plugins/kyberforge/.apm/skills/apm-workflow/references/marketplace.md`. This is a defect, not a
|
||||
style: `apm.yml`'s comment inside the marketplace block records that the top-level `version:` is not inherited into the compiled output
|
||||
"despite being used elsewhere (e.g. by `apm audit`)", so the field is live and was silently two
|
||||
releases behind what the marketplace published. Closed here rather than tracked, because the
|
||||
correction is one line and the drift is three days old.
|
||||
|
||||
**`docs/spec/architecture.md`'s plugin table is unchanged and stays a routing table.** It answers
|
||||
"where does a new skill go" for someone working *inside* this repo; the published description answers
|
||||
"should I install this" for someone outside it. The two now read similarly, and that is not
|
||||
duplication to collapse — they have different readers and different lifecycles, and the table already
|
||||
says so in its own preamble ("These are routing boundaries, not inventories"). One caveat for whoever
|
||||
next edits that page: its closing sentence sends a reader to the published description "for what a
|
||||
consumer actually gets", which was true against an enumeration and is now a pointer to a second
|
||||
boundary statement. Neither artifact carries an inventory after this change, so that sentence was
|
||||
rewritten in the same branch to point at `plugins/<name>/.apm/skills/` and `README.md` instead.
|
||||
|
||||
**`README.md`'s plugin bullet list becomes the only place an inventory lives, and it still
|
||||
enumerates.** That is deliberate, but it makes the list load-bearing in a way it was not before, so
|
||||
its `bin`, `git` and `gitea` bullets were completed in the same branch to name every skill those
|
||||
plugins ship. This ADR does not otherwise extend to it: a README is a hand-read document where a
|
||||
list of what you get is the useful thing, it is not compiled into four files, and a stale line in it
|
||||
costs a reader a moment rather than misrepresenting a published package. The tradeoff that makes
|
||||
enumeration wrong in a marketplace manifest is precisely the one that makes it fine there.
|
||||
|
||||
**Nothing in the ADR-0020 gate set changes.** Its character and word tiers, its Vale rules and its
|
||||
three validators all read `SKILL.md` and `*.agent.md` frontmatter; none of them opens an `apm.yml`.
|
||||
The two contracts are adjacent and independent, and a future author retrofitting a skill under
|
||||
issue #99 is not touched by this ADR.
|
||||
|
||||
**The failure mode this leaves open is a wrong boundary, and it is un-gated by design.** If a fourth
|
||||
failure of this class occurs it will be a description that describes the wrong plugin — `bb9158d`'s
|
||||
shape, the one enumeration never addressed. That is the trigger to revisit, and the thing to build
|
||||
then is a declared skill-to-boundary mapping, not a return to enumeration.
|
||||
@@ -21,11 +21,26 @@ project repo (local overrides)
|
||||
|
||||
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) 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
|
||||
|
||||
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.
|
||||
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). Self-contained is a hard constraint, not a description: a plugin is copied to a cache on install, so nothing inside it may reference a file outside its own directory. That is why the Vale styles are duplicated across two skills rather than shared (ADR-0014), and why ADR-0020's constants are copied into three validators rather than sourced from one. 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.
|
||||
|
||||
Which plugin a new skill belongs in follows from what each one is scoped to. The boundary that matters most in practice is `core` vs `kyberforge`: `core` is the home for cross-cutting, repo-agnostic utility skills that a consumer would want against *their* repo, while `kyberforge` is meta-tooling for the holocron marketplace itself. A skill that authors a target repo's `AGENTS.md` is `core`; a skill that audits a `SKILL.md` against this marketplace's contract is `kyberforge`.
|
||||
|
||||
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. 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 |
|
||||
|---|---|
|
||||
| `core` | Authoring and auditing a repo's `AGENTS.md` and the provider adapter files that defer to it |
|
||||
| `git` | Git operations and git hook tooling — anything driven over the git wire protocol against a local clone, plus the pre-commit hooks that guard it |
|
||||
| `gitea` | Anything reached through the Gitea HTTP API rather than the git wire protocol — the forge's own objects |
|
||||
| `kyberforge` | Creating and maintaining a Claude Code / Copilot CLI plugin marketplace — this repo's own meta-tooling |
|
||||
| `lint` | Configuring and running linters against a target repo; repo-agnostic, first linter is Vale |
|
||||
| `bin` | Unsorted skills that have not earned a home yet |
|
||||
|
||||
Two compilers produce the plugin roots you see in the tree:
|
||||
|
||||
@@ -40,6 +55,8 @@ That immunity is positional, not by filename. Anything placed *inside* a mirrore
|
||||
|
||||
`core/instructions/governance.md` is the always-on governance instruction file. Unlike the on-demand instruction files in the content index, governance.md is loaded into every Claude session via `@import` in `providers/claude-code/CLAUDE.md`. This is a technical guarantee, not a behavioural instruction — `@import` causes Claude Code to expand and load the file at launch, before any interaction begins.
|
||||
|
||||
Those on-demand files are plain markdown — no frontmatter, no schema. The agent decides when to read each one from task context and the content index label alone. Frontmatter is deferred until there is evidence that agents are loading the wrong files in practice; it is a deliberate deferral, not an oversight to close.
|
||||
|
||||
The governance layer has two phases:
|
||||
- **Phase 1** (complete): instruction and documentation layer — `governance.md` loaded via `@import`; `docs/ai-constitution.md` and `docs/wiki/HUMANS.md` as human-facing reference; `CONTEXT.md` extended with governance domain language.
|
||||
- **Phase 2** (planned): deterministic enforcement layer — pre-commit hooks, CI gates, secret scanning, licence scanning. Specified in `docs/research/governance_principles/CONTROLS.md`.
|
||||
@@ -57,10 +74,14 @@ This repo also has a `CLAUDE.md` at its root — the Claude Code entry point for
|
||||
|
||||
`CONTEXT.md` is therefore **not** always-loaded. `AGENTS.md` instructs agents to read it at session start, which is a behavioural instruction, not an `@import` guarantee — `LESSONS.md`'s 2026-05-17 entry proposed adding the import and it was never applied. Treat that entry as open work rather than a record of a landed change.
|
||||
|
||||
## 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 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.
|
||||
|
||||
## Provider model
|
||||
|
||||
`core/` is never tool-specific. `providers/` is never shared. When adding a new provider, write an adapter in `providers/<name>/` that translates core content into the tool's expected format and location. The core content itself does not change.
|
||||
|
||||
## Architectural decisions
|
||||
|
||||
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).
|
||||
Key hard-to-reverse decisions are recorded as ADRs in `docs/adr/`. There is no index file — the directory holds 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).
|
||||
@@ -0,0 +1,689 @@
|
||||
# Enforcement gates
|
||||
|
||||
Reference for this repo's pre-commit and pre-push hooks: what each one guards, what its numbers
|
||||
mean, and which shapes were tried and rejected. Read it when a gate fails, before changing anything
|
||||
in `.pre-commit-config.yaml`, or before "fixing" something that looks like an inconsistency — several
|
||||
of the oddities documented here are load-bearing and have already been re-litigated once.
|
||||
|
||||
`AGENTS.md` carries only the operative rules an agent needs in the moment. The reasoning lives here.
|
||||
|
||||
---
|
||||
|
||||
## Running the gates
|
||||
|
||||
| Command | Scope |
|
||||
|---|---|
|
||||
| `pre-commit run --all-files` | the commit-stage hooks |
|
||||
| `pre-commit run --hook-stage pre-push --all-files` | the push gate, one command — with one caveat below |
|
||||
| `pre-commit run skill-size-check --all-files` | just the ADR-0020 size/context gates |
|
||||
|
||||
Install 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` (everything below).
|
||||
|
||||
The pre-push command reports **16** hooks, not 14. The extra two are pre-commit's own `meta` hooks,
|
||||
`check-hooks-apply` and `check-useless-excludes`: they declare no `stages:`, so they run at every
|
||||
stage including this one. Both are declared in this repo's `.pre-commit-config.yaml` like everything
|
||||
else — what separates them is `repo: meta` (pre-commit's own built-ins) from `repo: local`. Fourteen
|
||||
is the count of hooks this repo authors itself.
|
||||
|
||||
**The caveat: one of those 14 is a silent no-op under that invocation.**
|
||||
`check-release-needed` exits 0 immediately unless `PRE_COMMIT_REMOTE_BRANCH` equals
|
||||
`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
|
||||
|
||||
Fourteen hooks, grouped below by what they guard rather than by the order `.pre-commit-config.yaml` declares them in.
|
||||
|
||||
**Core checks**
|
||||
|
||||
| Hook | Guards |
|
||||
|---|---|
|
||||
| `run-tests` | `bash tests/run-tests.sh --strict` — the whole suite, skips fatal (see [Tests](#tests)) |
|
||||
| `check-manifests` | `marketplace.json` and `plugin.json` paths resolve (needs `jq`) |
|
||||
|
||||
**Generated-content drift gates**
|
||||
|
||||
| Hook | Guards |
|
||||
|---|---|
|
||||
| `check-plugin-content-sync` | each plugin's flat `skills/agents/commands/hooks` mirror matches `.apm/` (issue #90) |
|
||||
| `check-marketplace-mirror-sync` | `.github/plugin/marketplace.json` is byte-identical to `.claude-plugin/marketplace.json` — no apm output profile targets that path |
|
||||
| `check-vale-style-sync` | skill-audit's Vale copy matches agent-audit's canonical copy, plus six glob-coverage probes (see [Vale](#vale)) |
|
||||
| `check-scope-walkup-sync` | `validate.sh`, `validate-provenance.sh`, `new-agent.sh` and `new-skill.sh`'s four independent `$HOME`/`.git`/`apm.yml` walk-up ports still agree behaviorally |
|
||||
| `check-executables-allow-sync` | root `apm.yml`'s `executables.allow` key names kyberforge's actual version (see [apm gates](#apm-gates)) |
|
||||
|
||||
`check-executables-allow-sync` is the odd one in this group: it guards a *silent failure* rather than
|
||||
drift in generated text.
|
||||
|
||||
**Artifact validators**
|
||||
|
||||
| Hook | Guards |
|
||||
|---|---|
|
||||
| `check-apm-agents-valid` | runs agent-audit's `validate.sh` over every real `plugins/*/.apm/agents/*.agent.md` (see [Agent files](#agent-files-take-the-description-gates-not-the-body-gate)) |
|
||||
|
||||
**apm's own gates**
|
||||
|
||||
| Hook | Guards |
|
||||
|---|---|
|
||||
| `apm-marketplace-check` | every `marketplace.packages[]` entry resolves, including network reachability of remote refs |
|
||||
| `apm-audit-ci` | `apm audit --ci` once per manifest — root plus each of the six plugin packages |
|
||||
| `apm-pack-check-clean` | `apm pack --check-versions --check-clean --dry-run` — the compiled marketplace still matches what `apm.yml` + `.apm/` would generate, and per-package versions agree with the `per_package` strategy |
|
||||
|
||||
**Host validators** (both need the `claude` CLI on PATH)
|
||||
|
||||
| Hook | Guards |
|
||||
|---|---|
|
||||
| `validate-plugins` | `claude plugin validate --strict` on every plugin directory |
|
||||
| `validate-marketplace` | `claude plugin validate --strict` on the root marketplace manifest |
|
||||
|
||||
**Release**
|
||||
|
||||
| Hook | Guards |
|
||||
|---|---|
|
||||
| `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`,
|
||||
and `check-plugin-content-sync` (via `scripts/sync-plugin-content.sh`, which wraps `apm pack`). The
|
||||
first and third are bare `apm …` entries and the second is a `bash -c` loop calling `apm` once per
|
||||
package, so without the CLI the push dies with an unhelpful "command not found". Install with
|
||||
`apm-install`, or `curl -sSL https://aka.ms/apm-unix | sh`; verify with `apm --version`. `jq` is
|
||||
needed by `scripts/check-manifests.sh` and `scripts/sync-plugin-content.sh` — those at least fail
|
||||
loudly (`Error: jq is required but not installed`).
|
||||
|
||||
## Skill and agent context gates (ADR-0020)
|
||||
|
||||
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 the
|
||||
`plugins/kyberforge/docs/research/examples/` reference skills. It is also shipped to external repos
|
||||
as `kyberforge-skill-size-check` (see
|
||||
[External consumers](#external-consumers-the-root-pre-commit-hooksyaml)).
|
||||
|
||||
### Two independent gate families, neither replaced the other
|
||||
|
||||
**Family 1 — agentskills.io spec backstop** (unchanged, conformance not quality):
|
||||
|
||||
| Constant | Value | Measured over |
|
||||
|---|---|---|
|
||||
| `MAX_LINES` | 500 | whole file, **frontmatter included** |
|
||||
| `MAX_WORDS` | 2,770 | whole file, **frontmatter included** |
|
||||
|
||||
**Family 2 — ADR-0020 context budget** (measured differently, on purpose):
|
||||
|
||||
| Check | SUGGESTION | FAIL | Measured over |
|
||||
|---|---|---|---|
|
||||
| `description` characters | 250 | 400 | the YAML-**folded** value |
|
||||
| body words | 600 | 900 | **body only** — everything after the frontmatter's closing `---` |
|
||||
|
||||
Plus two hard FAILs with no suggestion tier:
|
||||
|
||||
- **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
|
||||
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
|
||||
below early-returned on the genuinely empty folded value — exit 0, zero output, on a blocking gate.)
|
||||
- **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
|
||||
stack notices it.
|
||||
|
||||
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.
|
||||
|
||||
### 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` for any name; `-> name` only when the name is hyphenated — see the gap below) **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.
|
||||
|
||||
**Known gap: the arrow form only works for hyphenated names.** Target extraction is built on
|
||||
`NAME_HYPH` (`scripts/skill-size-check.sh:543`), which requires at least one hyphen, and
|
||||
`ARROW_BOUNDARY` (`:561`) inherits that. So `-> gitea-prs` is extracted and checked, while
|
||||
`-> triage` is not extracted at all — no ERROR, no SUGGESTION, exit 0. The unicode arrow `→` is not
|
||||
recognised in either case. This makes the SUGGESTION's own advice unsafe for a single-word skill:
|
||||
taking it silences the finding rather than checking it. `/name` has no such restriction and is the
|
||||
form to prefer. Tracked as a defect; `tests/test-adr0020-targets.sh` has one arrow case and its
|
||||
target happens to be hyphenated, so nothing currently covers this.
|
||||
|
||||
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
|
||||
|
||||
Resolution walks up **from the file being checked** — never from the script's own location. Deriving
|
||||
it from `${BASH_SOURCE}` leaked holocron's 39-skill universe into every consumer repo running the
|
||||
hook through pre-commit, so a consumer skill routing to `skill-audit` resolved against a plugin it
|
||||
had never installed.
|
||||
|
||||
The walk finds an **authoring root**: the nearest ancestor holding `plugins/*/.apm/skills` or
|
||||
`plugins/*/.apm/agents`, falling back to the nearest ancestor holding `.git`. **Two passes, not one
|
||||
interleaved walk**, so a nested `.git` (a submodule, a sub-package worktree) cannot beat a real
|
||||
monorepo root further up.
|
||||
|
||||
The universe is then:
|
||||
|
||||
1. every skill and agent under `<root>/plugins/*/` — sibling plugins resolve, which is what a
|
||||
monorepo means;
|
||||
2. the checked file's own apm package;
|
||||
3. the packages that package declares in **its own** `apm.yml` `dependencies.apm`.
|
||||
|
||||
The **root** manifest's `dependencies:` block is not read, and no plugin here declares a cross-plugin
|
||||
apm dependency — none needs to.
|
||||
|
||||
Deployed `.claude/` / `.agents/` trees are consulted **only** when the walk found no plugin monorepo
|
||||
root, whether it landed on a bare `.git` ancestor or on nothing at all. That is the consumer case.
|
||||
|
||||
**The gate keys on which of the two passes matched, never on whether the root contributed a new
|
||||
name.** A name-count delta looks equivalent and is not: `_collect_authoring_root()` re-collects the
|
||||
checked file's own plugin, whose names the earlier steps already added, so a single-plugin monorepo
|
||||
shows a delta of zero and would wrongly reach for the deployed trees — including the user's global
|
||||
`~/.claude/skills`, making the verdict depend on what happens to be installed.
|
||||
|
||||
Why it matters: those trees are gitignored `apm install` output, present only on a machine that has
|
||||
run it. Four cross-plugin targets here (`gitea-branches` → `git-branches`, `gitea-branches` →
|
||||
`git-history`, `gitea-issues` → `git-branches`, `gitea-workflow` → `git-workflow`) once resolved
|
||||
through `.claude/skills/` alone, so **the same commit measured 2 dangling targets on a developer
|
||||
machine and 6 on a fresh clone**. A gate shipping hot with no baseline cannot give two answers.
|
||||
|
||||
Verified fixed: running the hook over a tree holding only `plugins/` and the root `apm.yml`, with no
|
||||
`.claude/` or `.agents/` anywhere, produces findings identical to the working tree — **26 description
|
||||
FAILs, 9 body FAILs, 2 dangling targets, 0 missing references, 58 SUGGESTIONs**.
|
||||
|
||||
### SUGGESTION-only checks
|
||||
|
||||
Three more, deterministic to measure but judgment to act on:
|
||||
|
||||
- a description with **no boundary clause at all**;
|
||||
- a `## Gotchas` section with **more than five entries**;
|
||||
- a `## Gotchas` section over **25% of the body**.
|
||||
|
||||
### `verbose: true` is load-bearing
|
||||
|
||||
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
|
||||
suggestion is swallowed, which is exactly the invisibility ADR-0013 records for Vale warnings.
|
||||
ADR-0020's preload arithmetic depends on it: writing to the 400-char FAIL delivers roughly half the
|
||||
cut that writing to the 250-char SUGGESTION does, so the intended saving depends entirely on that
|
||||
tier being visible. The numbers, and the measurement method behind them, are not restated here —
|
||||
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
|
||||
|
||||
`skill-audit`'s `validate.sh` holds a second copy of the four ADR-0020 constants
|
||||
(`DESC_SUGGEST_CHARS` / `DESC_MAX_CHARS` / `BODY_SUGGEST_WORDS` / `BODY_MAX_WORDS`), and
|
||||
`agent-audit`'s `validate.sh` holds a third copy of the two description constants. They are copied
|
||||
rather than imported because a cache-installed plugin's scripts cannot read files outside their own
|
||||
plugin directory. `tests/test-skill-size-check.sh` asserts the copies agree, so drift fails CI rather
|
||||
than silently letting an audit bless a skill the commit hook then rejects. The shared boundary
|
||||
resolver block is embedded verbatim in all three scripts between `BEGIN`/`END ADR-0020 SHARED
|
||||
BOUNDARY RESOLVER` markers and must stay byte-identical.
|
||||
|
||||
### `python3` and PyYAML are hard requirements
|
||||
|
||||
Both, and neither is a best-effort accelerator.
|
||||
|
||||
`python3` because the script measures the **folded** `description` value. Most descriptions here are
|
||||
`>`-block scalars, so a regex over the raw lines measures indentation and newlines instead of the
|
||||
value. Missing it fails the hook with an install pointer rather than skipping the ADR-0020 checks,
|
||||
which would be a vacuous green. In practice it is already present — pre-commit is itself a Python
|
||||
application.
|
||||
|
||||
**PyYAML** because the hand-rolled fallback frontmatter reader has been **removed deliberately**. It
|
||||
disagreed with a real parser across the FAIL boundary — one corpus description measured 270
|
||||
characters parsed and 412 unparsed — and a quoted `"description"` key or an explicit
|
||||
`description: null` returned empty from it, silently skipping the description *and* routing checks. A
|
||||
reader that mis-parses an unfamiliar scalar shape reports a clean pass on a file it never measured,
|
||||
which is the exact vacuous-green failure the `python3` check exists to avoid. `pip install pyyaml`
|
||||
(or `python3 -m pip install PyYAML`, or the distro's `python3-yaml`) if the hook reports it missing.
|
||||
|
||||
## Agent files take the description gates, not the body gate
|
||||
|
||||
`check-apm-agents-valid` runs agent-audit's `validate.sh` over every real
|
||||
`plugins/*/.apm/agents/*.agent.md`. It derives its expected file set from `git ls-files` — the pattern
|
||||
`tests/run-bats.sh` established — so an agent file deleted from the worktree but still tracked fails
|
||||
the run, and **discovering zero agent files is an error, not 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.
|
||||
|
||||
The hook exists because `validate.sh` was previously exercised only by `check-scope-walkup-sync`,
|
||||
against synthetic `mktemp` fixtures — it had never run against the agent files it governs. That is
|
||||
how ADR-0016 could be amended to bless a `disallowedTools` frontmatter field while `validate.sh`'s
|
||||
allowlist still rejected it: spec and enforcer disagreed and every gate stayed green.
|
||||
|
||||
Agents take the ADR-0020 **description** gates (agent-audit's `validate.sh` holds its own copy of
|
||||
those two constants) and, deliberately, **no body word gate**. A skill body is loaded into the
|
||||
caller's context and competes with the live conversation; an agent body becomes the system prompt of
|
||||
a *fresh* context. The rationale for the 900-word FAIL does not transfer. A bats test pins that
|
||||
absence in agent-audit's validator — adding a body gate there contradicts the ADR rather than fixing
|
||||
an inconsistency.
|
||||
|
||||
**Be precise about the scope of that guarantee: it holds for the *validator*, not for the shared
|
||||
script.** `scripts/skill-size-check.sh` applies its body gate to whatever path it is handed, and
|
||||
|
||||
```
|
||||
bash scripts/skill-size-check.sh plugins/*/.apm/agents/*.agent.md
|
||||
```
|
||||
|
||||
exits 1 today with 900-word body FAILs on `git-orchestrate` (933), `gitea-orchestrate` (1,199) and
|
||||
`apm-orchestrate` (1,080). Agent files escape only because the hook definitions filter on `SKILL.md`
|
||||
— a file-pattern accident that happens to implement the design, not the design itself. **Do not
|
||||
"extend" that hook's `files:` pattern to cover agents** on the assumption that the script already
|
||||
knows the difference; doing so silently enforces a gate ADR-0020 declines to set.
|
||||
|
||||
## Current retrofit status
|
||||
|
||||
**The ADR-0020 gates ship hot, with no baseline file.** A shrinking baseline recording each
|
||||
non-compliant skill's current numbers was considered and rejected in favour of hot gates.
|
||||
|
||||
Two independent hot gates are currently red, and the first will not warn you about the second.
|
||||
|
||||
| Gate | Current findings |
|
||||
|---|---|
|
||||
| `skill-size-check` | **26 of 39** descriptions and **9 of 39** bodies exceed their FAIL tier; 2 dangling targets; 58 SUGGESTIONs |
|
||||
| `Kyberforge.CompositionNote` (Vale) | **10 errors across four skills**: `gitea-issues`, `gitea-labels-milestones`, `gitea-prs`, `gitea-workflow` |
|
||||
|
||||
`Kyberforge.CompositionNote` is the ADR-0020 Vale rule banning composition and architecture prose
|
||||
from a description. Every Vale rule here is `level: error` with no ignorable tier, so touching any of
|
||||
those four skills means fixing its prose findings as well as its size findings.
|
||||
|
||||
Consequence: editing a non-compliant skill *for any reason* means retrofitting it to the contract
|
||||
first — a one-line fix to `gitea-prs` cannot be committed until that skill complies. This is
|
||||
deliberate; it guarantees convergence and avoids a half-state. Tracked as Gitea issue **#99**.
|
||||
|
||||
Check where a skill stands before starting, and check **both** gates:
|
||||
|
||||
```
|
||||
pre-commit run skill-size-check --all-files # size/context only
|
||||
pre-commit run --all-files # size AND Vale
|
||||
```
|
||||
|
||||
Scoping a retrofit off `skill-size-check` output alone leaves you blocked at the second gate.
|
||||
|
||||
## Vale
|
||||
|
||||
Install the `vale` binary — `brew install vale` (macOS), `snap install vale` (Linux),
|
||||
`choco install vale` (Windows), or see <https://vale.sh/docs/vale-cli/installation/>. No `vale sync`
|
||||
is needed: the `Kyberforge` styles are **committed** under
|
||||
`plugins/kyberforge/.apm/skills/{skill-audit,agent-audit}/assets/vale/styles/`, not downloaded
|
||||
packages (ADR-0014).
|
||||
|
||||
### Two copies, one canonical
|
||||
|
||||
Wiring Vale as a deterministic prefilter for `skill-audit`/`agent-audit`'s Description dimension
|
||||
(motivation: issue #84) is repo-specific, not part of the generic `lint` plugin, so it does not live
|
||||
in `plugins/lint/` — and per ADR-0014 it no longer lives at the repo root either. It lives **twice**,
|
||||
one copy per skill, both under `plugins/kyberforge/.apm/skills/`:
|
||||
|
||||
| Copy | Styles | `.vale.ini` sections |
|
||||
|---|---|---|
|
||||
| `agent-audit/assets/vale/` — **canonical** | `Kyberforge`, `KyberforgeCopilot` | `[**/agents/*.md]`, `[**/*.agent.md]` |
|
||||
| `skill-audit/assets/vale/` — smaller duplicate | `Kyberforge` | `[**/SKILL.md]` |
|
||||
|
||||
Duplicated rather than shared because a plugin's cache-install copies only each skill's own files —
|
||||
there is no cross-skill sharing to point at. `check-vale-style-sync` at pre-push is what keeps them
|
||||
from drifting; `KyberforgeCopilot` is the one deliberate inequality, being scoped only to `.agent.md`
|
||||
files for the Copilot-only "`Use proactively` has no effect" check.
|
||||
|
||||
### What Vale owns, and what stays LLM judgment
|
||||
|
||||
Eleven rule files across the two copies, six distinct rules:
|
||||
|
||||
| Rule | Vale scope | Bans | From |
|
||||
|---|---|---|---|
|
||||
| `Kyberforge.DescriptionOpener` | `text.frontmatter.description` | non-imperative openers ("This skill/agent…") | issue #84 |
|
||||
| `Kyberforge.VagueWording` | `text.frontmatter.description` | vague capability wording ("helps with", "utilize", …) | issue #84 |
|
||||
| `Kyberforge.PaddingPhrase` | `text` | generic "see `references/` for details" padding | issue #84 |
|
||||
| `KyberforgeCopilot.ProactivePhrase` | `text.frontmatter.description` | `Use proactively` (no effect in Copilot) | issue #84 |
|
||||
| `Kyberforge.SentenceOpenerThereIs` | `sentence` | "There is/are" sentence openers | ADR-0013 |
|
||||
| `Kyberforge.CompositionNote` | `text.frontmatter.description` | architecture and composition prose in a description | ADR-0020 |
|
||||
|
||||
Vale covers the **pattern-matchable** sub-checks named in issue #84 plus, per ADR-0013, one
|
||||
cherry-picked body-wide prose-pattern rule. Everything else stays LLM judgment: defaults-vs-menus,
|
||||
why-rationale, the non-pattern-matchable body-discipline calls, near-miss exclusion strength, and
|
||||
control calibration. New rules land directly in `styles/Kyberforge` and block immediately — there is
|
||||
no trial tier.
|
||||
|
||||
The cherry-pick record, so it is not re-litigated:
|
||||
|
||||
- `Kyberforge.SentenceOpenerThereIs` **landed** — 22 held-out hits, both in-corpus hits clean
|
||||
rewrites, zero suppressions needed.
|
||||
- `Kyberforge.VagueQualifier` was cherry-picked and then **deleted**. 2 hits across the corpus as it
|
||||
stood on 2026-08-08 (before the `.apm/` restructure): one marginal, and one unfixable false
|
||||
positive — `caveman/SKILL.md` quotes `of course` as an example of filler, a mention rather than a
|
||||
use — which forced the repo's only Vale suppression comments.
|
||||
- `governance.md` and `CONTROLS.md` were evaluated as rule sources and **excluded**: nothing
|
||||
prose-pattern-matchable to mine.
|
||||
|
||||
### Why every rule is `level: error`
|
||||
|
||||
Every alert is a FAIL, with no ignorable tier — same all-or-nothing model as shellcheck, the test
|
||||
suite, and conventional-pre-commit. Graded severities do not work here: **Vale's exit code keys on
|
||||
`error` alerts alone**, so a `warning` or `suggestion` rule exits 0, and pre-commit swallows a
|
||||
passing hook's output. Such a rule would be invisible and would block nothing.
|
||||
|
||||
`MinAlertLevel` and `--minAlertLevel` are correspondingly **absent** from both `.vale.ini` files and
|
||||
from the hook definitions. Under this model they are no-ops; adding one is not a missing knob.
|
||||
|
||||
The `verbose: true` escape hatch that makes `skill-size-check`'s SUGGESTION tier audible has no
|
||||
analogue here — Vale has no tier to make audible.
|
||||
|
||||
### External consumers: the root `.pre-commit-hooks.yaml`
|
||||
|
||||
The root `.pre-commit-hooks.yaml` exposes both Vale copies (`kyberforge-vale-audit-skill`,
|
||||
`kyberforge-vale-audit-agent`) plus `kyberforge-skill-size-check`, so any external repo can enforce
|
||||
the same rules with `repo: <this-repo-url>, rev: <tag>` in its own `.pre-commit-config.yaml`.
|
||||
pre-commit clones the pinned rev into its own cache, independent of whether Claude Code or the
|
||||
`kyberforge` plugin is installed at all; the same mechanism covers CI via `pre-commit run
|
||||
--all-files`. `skill-size-check` has no external asset dependency, so it needed no relocation under
|
||||
ADR-0014 — only exposure.
|
||||
|
||||
This repo's own `vale-audit-prefilter-skill` / `-agent` hooks consume the **identical**
|
||||
plugin-bundled copies via `repo: local`. Deliberately not a third root copy, and deliberately **not a
|
||||
pinned self-reference** — a pinned self-reference would lint working-tree edits against the last
|
||||
tagged release rather than against the change being made.
|
||||
|
||||
### Pre-commit
|
||||
|
||||
Two prefilter hooks, with `.apm/`-scoped `files:` patterns:
|
||||
|
||||
| Hook | Pattern |
|
||||
|---|---|
|
||||
| `vale-audit-prefilter-skill` | `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$` |
|
||||
| `vale-audit-prefilter-agent` | `^plugins/[^/]+/\.apm/agents/[^/]+\.agent\.md$` |
|
||||
|
||||
Only the **authoring source** triggers them. A `SKILL.md` in the generated flat mirror matches
|
||||
neither pattern, so prose findings surface only when you edit the file you are supposed to be
|
||||
editing. Without the binary the hooks fail with a bare "command not found" and no install pointer.
|
||||
|
||||
**Two hooks, not one combined hook.** Both manifests split the prefilter in two precisely because a
|
||||
single hook can point at only one copy, and that copy would silently 0-file-skip the other file
|
||||
shape (see [A 0-file Vale run is NOT RUN](#a-0-file-vale-run-is-not-run)).
|
||||
|
||||
### The `.vale.ini` globs do no scoping
|
||||
|
||||
Each `.vale.ini`'s section globs are **path-agnostic** — `[**/SKILL.md]` for skill-audit's copy,
|
||||
`[**/agents/*.md]` and `[**/*.agent.md]` for agent-audit's — and constrain filename *shape*, not
|
||||
location: Vale's `*` crosses `/`. A `SKILL.md` outside `plugins/` (a project-scope
|
||||
`.claude/skills/foo/SKILL.md`, say) still matches `[**/SKILL.md]` and gets linted normally.
|
||||
|
||||
All scoping therefore comes from the pre-commit hook's own `files:` regex and from the audit skills
|
||||
passing one explicit file per invocation. The two manifests scope **differently on purpose**:
|
||||
|
||||
| Manifest | `-skill` | `-agent` |
|
||||
|---|---|---|
|
||||
| `.pre-commit-config.yaml` (pins this repo's layout) | `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$` | `^plugins/[^/]+/\.apm/agents/[^/]+\.agent\.md$` |
|
||||
| `.pre-commit-hooks.yaml` (layout-agnostic for consumers) | `(^\|/)SKILL\.md$` | `(^\|/)agents/[^/]+\.md$\|\.agent\.md$` |
|
||||
|
||||
Narrowing a `.vale.ini` glob to a `plugins/`-shaped path to "tighten" it breaks the consumer case,
|
||||
and `check-vale-style-sync`'s probe set is built to catch exactly that.
|
||||
|
||||
### `vale-wrap.sh`, never bare `vale`
|
||||
|
||||
Both audit skills' Step 1 and both pre-commit hooks call **each copy's own**
|
||||
`scripts/vale-wrap.sh`, not `vale`. It works around a confirmed **Vale 3.15.2** limitation:
|
||||
`text.frontmatter.description` silently stops matching on most — not all — multi-line descriptions.
|
||||
|
||||
Verified by reproduction on a deliberately-bad fixture, not assumed:
|
||||
|
||||
| Description scalar spanning 2+ lines | Vale's behaviour |
|
||||
|---|---|
|
||||
| `>` folded block | 0 alerts, exit 0 — **broken** |
|
||||
| plain (unquoted) continuation lines | 0 alerts, exit 0 — **broken** |
|
||||
| single- or double-quoted, wrapped | 0 alerts, exit 0 — **broken** |
|
||||
| `\|` literal block | alerts fire, exit 1 — lints normally |
|
||||
|
||||
The wrapper flattens the three broken forms to a single-line scalar in a scratch copy — or, for the
|
||||
rare value no inline scalar can spell verbatim, a `|-` block with one content line — padding with
|
||||
blank lines so **every other line number is unchanged**. `|` literal blocks and single-line
|
||||
descriptions pass through untouched. Most descriptions in this repo are `>` blocks, so before the
|
||||
wrapper a bad description in any of the three broken forms sailed straight through the prefilter.
|
||||
|
||||
### The `--config` argv defect
|
||||
|
||||
Handed **no `--config` at all**, the wrapper falls back to its own sibling `assets/vale/.vale.ini`,
|
||||
located from `${BASH_SOURCE[0]}` rather than from the cwd. That is why both manifests' `entry:` is
|
||||
now the bare script path with **no argument after it**.
|
||||
|
||||
pre-commit prefixes only `entry[0]` with the hook-repo clone path (`cmd = (prefix.path(cmd[0]),
|
||||
*cmd[1:])`), so every later argument resolves against the **consuming** repo's root. A `--config` in
|
||||
`.pre-commit-hooks.yaml` therefore pointed at a path no consumer has and hard-failed every external
|
||||
run with `E100 [--config] Runtime error`.
|
||||
|
||||
`.pre-commit-config.yaml` drops the argument too, deliberately keeping the two entries identical.
|
||||
The local `repo: local` hook resolved its `--config` correctly only because the consuming repo *was*
|
||||
this repo — and that divergence is why three review rounds exercised a path no external consumer
|
||||
takes and missed the defect. **Do not reintroduce a `--config` to either manifest to make the local
|
||||
run "explicit".**
|
||||
|
||||
An explicit `--config` from any other caller still wins, in all three argv forms (`--config X`,
|
||||
`--config=/abs`, `--config=rel`), and a relative one resolves against the caller's cwd — matching
|
||||
bare `vale`, not the repo root.
|
||||
|
||||
Both audit skills' Step 1 passes no `--config` either. Step 1 resolves the script relative to the
|
||||
skill's own directory so the call works from an installed plugin cache; a relative `--config`
|
||||
alongside it would resolve against the cwd instead, yielding `E100 Runtime error … does not exist`
|
||||
and exit 2 — which both skills' fallback misreads as "vale unavailable" and silently downgrades to
|
||||
full LLM judgment.
|
||||
|
||||
`tests/test-vale-wrap.sh` regression-tests this against **skill-audit's** copy specifically: its
|
||||
fixtures are all `SKILL.md`-shaped, and only skill-audit's `.vale.ini` carries that glob section.
|
||||
|
||||
### A 0-file Vale run is NOT RUN
|
||||
|
||||
Vale reports 0 files only when the path it is handed matches **no glob section at all** — a
|
||||
differently-named file, or a directory argument holding nothing that matches. That run prints
|
||||
|
||||
```
|
||||
✔ 0 errors ... in 0 files.
|
||||
```
|
||||
|
||||
and exits 0, indistinguishable from a clean pass. Both audits therefore treat a 0-file Vale run as
|
||||
**NOT RUN** and fall back to full LLM judgment rather than reporting the Description dimension
|
||||
clean.
|
||||
|
||||
### Pre-push
|
||||
|
||||
`vale` is a **pre-push** dependency too, not only pre-commit. `check-vale-style-sync` runs **six
|
||||
glob-coverage probes** by invoking `vale --config` — one representative path per file shape the
|
||||
prefilter is supposed to cover. They are the only assertions in the script that catch a `.vale.ini`
|
||||
glob typo (`[**/SKILL.md]` → `[**/SKILLS.md]`), the failure mode where every text-level check stays
|
||||
clean while vale lints zero files. As a warning this self-disabled on exactly that mutation and
|
||||
exited 0, and since pre-commit swallows a passing hook's output the stderr line was never seen — the
|
||||
hook reported `Passed`. Missing `vale` is therefore a hard failure here.
|
||||
|
||||
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 line as "the glob axis
|
||||
was not checked", not as a pass. The hook is `verbose: true` for exactly that reason — its clean
|
||||
output is a single line, so it costs one line per push.
|
||||
|
||||
### Mentioning banned phrasing without tripping the rule
|
||||
|
||||
House convention: banned phrasing that must be **mentioned** rather than used goes in backticks or a
|
||||
fenced code block. Vale skips code spans and fences, so no suppression is needed — which is why this
|
||||
document quotes `Use proactively` and "There is/are" the way it does.
|
||||
|
||||
Inline `<!-- vale Rule = NO -->` is the fallback **only** where backticking is impossible. Use the
|
||||
HTML-comment form; the MDX `{/* */}` form does not work in plain Markdown. The one time a rule forced
|
||||
suppression comments, the rule was deleted instead (see the `VagueQualifier` entry above).
|
||||
|
||||
## Tests
|
||||
|
||||
```
|
||||
bash tests/run-tests.sh # every test-*.sh plus the bats suite
|
||||
bash tests/run-tests.sh --bats-only # just bats
|
||||
```
|
||||
|
||||
First run auto-initializes the bats submodules; no manual `git submodule update` needed.
|
||||
|
||||
**Exit 77 = SKIPPED.** A suite that skips because a dependency is missing 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 documented dependencies is
|
||||
absent on this machine, so the gate would otherwise report success having run fewer suites than it
|
||||
appears to. Without `--strict` the gate once went green having verified 15 of 17 suites on a
|
||||
vale-less PATH, with the skip list swallowed. Without vale, 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 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.
|
||||
|
||||
## apm gates
|
||||
|
||||
### `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
|
||||
dependency entry passes `apm pack --check-versions --check-clean --dry-run` and fails
|
||||
`apm audit --ci` in that package's directory. Costs ~0.5s per package.
|
||||
|
||||
It verifies **exactly two things** per manifest and claims no more:
|
||||
|
||||
- **manifest-parse** — each `apm.yml` parses as a valid APM manifest. Unconditional; verified to fire
|
||||
on a dependency entry missing its `git`/`path`/`registry` field (`Cannot parse apm.yml`).
|
||||
- **lockfile-exists** — any package declaring dependencies has a consistent `apm.lock.yaml`.
|
||||
Conditional, and vacuous while every plugin `apm.yml` declares `dependencies: {apm: [], mcp: []}`;
|
||||
it arms itself the moment one does not (verified by adding a git dependency to
|
||||
`plugins/lint/apm.yml`).
|
||||
|
||||
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`.** apm's own message
|
||||
suggests it; it was tried on a scratch copy and **rejected**. With no reachable policy source it does
|
||||
not make the check meaningful, it makes it permanently red — `apm audit --ci` exits 1 with
|
||||
`No org policy found at unknown (policy.fetch_failure_default=block)` on every push, forever. A gate
|
||||
that can never go green is not a gate. Revisit only if this repo gains a policy source apm can reach.
|
||||
|
||||
It also does not scan for hidden Unicode: that scan is plain `apm audit`, a different mode (`--ci`
|
||||
refuses to combine with `--file`/`--strip`/`--dry-run`/`PACKAGE`), and plain `apm audit` here reports
|
||||
`No apm.lock.yaml found -- nothing to scan` and exits 0. Adding it would buy a second vacuous check.
|
||||
|
||||
### `check-executables-allow-sync`
|
||||
|
||||
apm gates a package's `hooks/` and `bin/` on an **exact `<package>#<version>` dictionary lookup** in
|
||||
root `apm.yml`'s `executables.allow` (`apm_cli/security/executables.py`, `is_package_approved`).
|
||||
There is no wildcard and no version-less form.
|
||||
|
||||
So bumping `plugins/kyberforge/apm.yml`'s `version:` without bumping the key **errors nowhere**: the
|
||||
entry simply stops matching, the gate blocks the hook, kyberforge's `SessionStart` hook stops
|
||||
deploying, and the apm install goes quietly stale — the exact failure ADR-0019 exists to end,
|
||||
reintroduced through the mechanism meant to secure it. ADR-0019 records this as a live failure mode;
|
||||
the release that shipped the hook hit it immediately.
|
||||
|
||||
`scripts/check-executables-allow-sync.sh` parses `version:` out of `plugins/kyberforge/apm.yml` and
|
||||
asserts root `apm.yml` carries the matching `kyberforge#<version>` key. A comment in the
|
||||
`executables:` block stays as the human-facing pointer; the hook is what actually holds. It parses
|
||||
with PyYAML where importable and falls back to a two-shape scan otherwise, so a missing pip package
|
||||
cannot become the thing that blocks every push.
|
||||
|
||||
## `.claude/settings.json`
|
||||
|
||||
**apm owns this file. Nothing repo-authored goes in it.**
|
||||
|
||||
`apm audit --ci` replays the install into a scratch tree and diffs the result byte-for-byte, so
|
||||
anything apm would not have written there — an `enabledPlugins` block, a real `hooks` entry — is
|
||||
permanent drift that fails `apm-audit-ci`. A hook you want in this repo is authored in
|
||||
`plugins/<name>/.apm/hooks/` and deployed by apm, never hand-written here.
|
||||
|
||||
Its committed content is whatever apm last wrote, which today is the merged `SessionStart` entry for
|
||||
kyberforge's `check-apm-current.sh`. That is apm's own output and it belongs in the commit (ADR-0019;
|
||||
ADR-0018's statement that the committed content is exactly `{"hooks": {}}` is superseded on that
|
||||
point only). 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`.
|
||||
|
||||
### Why it is excluded from `pretty-format-json`
|
||||
|
||||
It is the **sixth and last alternation** in that hook's `exclude:` pattern, and the only one there
|
||||
for a reason other than "generated manifest". Mind which number you are quoting: **six alternations,
|
||||
expanding to sixteen real files** — 3 root marketplace manifests, 2 per plugin × 6 plugins, plus this
|
||||
one.
|
||||
|
||||
`pretty-format-json --autofix` sorts object keys unless `--no-sort-keys` is passed, while apm's hook
|
||||
integrator emits insertion order (`matcher` before `hooks`, `type` before `command`). Leaving the
|
||||
file in that hook's scope therefore rewrites apm's output into a form apm would never produce on the
|
||||
way into **every** commit, and `apm-audit-ci` then reports permanent drift on a file with an empty
|
||||
`git diff` — exactly what happened when the `SessionStart` hook first landed in `2e395a4`. Re-running
|
||||
`apm install` fixes the file; leaving it in scope would re-break it on the very commit carrying the
|
||||
fix.
|
||||
|
||||
**Load-bearing. Do not tidy it out of that list** (see `LESSONS.md`, 2026-08-14).
|
||||
|
||||
## Pushing without a network
|
||||
|
||||
Exactly **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`.
|
||||
|
||||
| Hook | Offline failure |
|
||||
|---|---|
|
||||
| `apm-marketplace-check` (`always_run`, resolves every entry) | `No cached refs (offline)` |
|
||||
| `apm-pack-check-clean` (re-resolves the same entry) | `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 twelve pre-push hooks pass offline
|
||||
because they are real local checks. (`check-executables-allow-sync` landed after that run, but reads
|
||||
two local manifests and makes no network call.) Adding any other hook 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.
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
- `docs/adr/0020-skill-description-and-body-context-contract.md` — the context contract, its
|
||||
enforcement table (deterministic vs. auditor judgment), and every rejected alternative
|
||||
- `docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md` — the `SessionStart` hook, the
|
||||
executable-trust gate, and the version-pinned allow key
|
||||
- `docs/adr/0017-plugin-content-mirror-bridges-apm-to-host-discovery.md`,
|
||||
`docs/adr/0015-apm-replaces-plugin-marketplace-authoring.md`,
|
||||
`docs/adr/0014-vale-prefilter-ships-from-the-plugin.md` — plugin content sync, apm-generated
|
||||
manifests, committed Vale styles
|
||||
- `docs/spec/architecture.md` — directory structure, install pipeline, what is generated and what is
|
||||
hand-authored
|
||||
- `.pre-commit-config.yaml` — the hooks themselves, with inline rationale comments
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bin",
|
||||
"version": "1.1.3",
|
||||
"description": "A place for things to be binned",
|
||||
"version": "1.1.5",
|
||||
"description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
"email": "[email protected]",
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bin",
|
||||
"version": "1.1.3",
|
||||
"description": "A place for things to be binned",
|
||||
"version": "1.1.5",
|
||||
"description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
"email": "[email protected]",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
name: bin
|
||||
version: 1.1.3
|
||||
description: A place for things to be binned
|
||||
version: 1.1.5
|
||||
description: Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.
|
||||
author:
|
||||
name: Defame1297
|
||||
email: [email protected]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "git",
|
||||
"version": "1.3.3",
|
||||
"description": "Skills for working with Git \u2014 conventional commits, branch management, pull requests, and feature flow.",
|
||||
"version": "1.3.5",
|
||||
"description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
"email": "[email protected]",
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "git",
|
||||
"version": "1.3.3",
|
||||
"description": "Skills for working with Git \u2014 conventional commits, branch management, pull requests, and feature flow.",
|
||||
"version": "1.3.5",
|
||||
"description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
"email": "[email protected]",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
name: git
|
||||
version: 1.3.3
|
||||
description: Skills for working with Git — conventional commits, branch management, pull requests, and feature flow.
|
||||
version: 1.3.5
|
||||
description: Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.
|
||||
author:
|
||||
name: Defame1297
|
||||
email: [email protected]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "gitea",
|
||||
"version": "1.3.4",
|
||||
"description": "Skills for managing Gitea repositories \u2014 issues, pull requests, milestones, releases, and wikis.",
|
||||
"version": "1.3.6",
|
||||
"description": "Skills and agents for working with a Gitea forge through its HTTP API \u2014 the forge's own objects, as distinct from the local git clone.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
"email": "[email protected]",
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "gitea",
|
||||
"version": "1.3.4",
|
||||
"description": "Skills for managing Gitea repositories \u2014 issues, pull requests, milestones, releases, and wikis.",
|
||||
"version": "1.3.6",
|
||||
"description": "Skills and agents for working with a Gitea forge through its HTTP API \u2014 the forge's own objects, as distinct from the local git clone.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
"email": "[email protected]",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: gitea
|
||||
version: 1.3.4
|
||||
description: Skills for managing Gitea repositories — issues, pull requests, milestones, releases, and wikis.
|
||||
version: 1.3.6
|
||||
description: Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.
|
||||
author:
|
||||
name: Defame1297
|
||||
email: [email protected]
|
||||
|
||||
@@ -76,6 +76,9 @@ Include what the fresh context lacks:
|
||||
- A direct role instruction opening the prompt: `You are a [role]. When invoked, [action].`
|
||||
- One bounded job, stated so the agent knows what it must refuse.
|
||||
- The dispatch, gates, inputs and outputs listed above.
|
||||
- **Error handling** — what the agent does on malformed, missing or contradictory input: stop and
|
||||
report, or degrade to a named fallback. Absent it, the agent invents a recovery, and a
|
||||
subagent's invented recovery is invisible to its caller until the output is wrong.
|
||||
- Non-obvious environment facts and project-specific conventions it cannot infer.
|
||||
- One default per decision point with one escape hatch.
|
||||
|
||||
@@ -111,6 +114,8 @@ Flag as FAIL if:
|
||||
Flag as SUGGESTION if:
|
||||
|
||||
- The body does not open with a direct role instruction
|
||||
- The body specifies no error handling — nothing tells the agent what to do with malformed,
|
||||
missing or contradictory input
|
||||
- The job the agent describes is unbounded, or bounded only implicitly
|
||||
- A rationale is missing from a rule the agent is expected to enforce — present but unexplained
|
||||
- Comments are useful but verbose enough to bury the field they annotate
|
||||
@@ -111,9 +111,12 @@ Flag as FAIL if:
|
||||
available). `Kyberforge.VagueWording` catches the known filler; imprecision outside that list is
|
||||
judgment.
|
||||
- **A boundary clause naming a target that does not resolve** to a real skill directory or agent
|
||||
file in the authoring source. No script checks this for an agent file — `validate.sh` resolves
|
||||
boundary targets for skills only, so resolve the name yourself against `plugins/*/.apm/skills/`
|
||||
and `plugins/*/.apm/agents/`.
|
||||
file in the authoring source. `validate.sh` resolves this for agent files at both scopes and
|
||||
reports each unresolved target itself — take its verdict rather than re-resolving the name by
|
||||
hand, because a hand-walk over a different universe can contradict it. What is left to you is
|
||||
semantic and the script cannot reach it: whether a target that *does* resolve is the right
|
||||
sibling to exclude, and whether a clause naming no target at all ("examine the files manually")
|
||||
should have named one.
|
||||
- **`Use proactively` in a Copilot or vendor-neutral description.**
|
||||
`KyberforgeCopilot.ProactivePhrase` catches it. The phrase steers the Claude Code runtime and
|
||||
does nothing anywhere else, so in a `.agent.md` it is preloaded text that buys no behaviour.
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -35,6 +35,24 @@ You are a test agent. When invoked, do the thing.
|
||||
EOF
|
||||
}
|
||||
|
||||
# Helper: a description of EXACTLY <n> characters that carries a boundary
|
||||
# clause and names no routing target. ADR-0020's missing-boundary-clause
|
||||
# SUGGESTION fires on any description without one, so a fixture that omits it
|
||||
# is never "otherwise clean" and a test refuting SUGGESTION would be asserting
|
||||
# the boundary check's absence instead of the thing it names. The clause is
|
||||
# paid for out of the measured budget rather than appended to it, because
|
||||
# these tests measure the description LENGTH. "anything else" is not
|
||||
# hyphenated, so no routing target comes with it.
|
||||
desc_of_length() {
|
||||
python3 - "$1" <<'PY'
|
||||
import sys
|
||||
n = int(sys.argv[1])
|
||||
prefix = 'Use when doing the thing. Do not use for anything else. '
|
||||
assert n >= len(prefix), 'requested description shorter than the boundary clause'
|
||||
print(prefix + 'x' * (n - len(prefix)))
|
||||
PY
|
||||
}
|
||||
|
||||
# Helper: same shape as make_apm_agent, but the description is supplied
|
||||
# verbatim — used by the ADR-0020 description-budget tests.
|
||||
make_apm_agent_with_desc() {
|
||||
@@ -637,7 +655,7 @@ EOF
|
||||
|
||||
@test "ADR-0020: agent description of exactly 250 chars raises no suggestion" {
|
||||
local root="$TMPDIR/pkg"
|
||||
make_apm_agent_with_desc "$root" "my-agent" "$(python3 -c "print('x' * 250)")"
|
||||
make_apm_agent_with_desc "$root" "my-agent" "$(desc_of_length 250)"
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_success
|
||||
refute_output --partial "SUGGESTION"
|
||||
@@ -645,7 +663,7 @@ EOF
|
||||
|
||||
@test "ADR-0020: agent description of 251 chars raises a SUGGESTION and still exits 0" {
|
||||
local root="$TMPDIR/pkg"
|
||||
make_apm_agent_with_desc "$root" "my-agent" "$(python3 -c "print('x' * 251)")"
|
||||
make_apm_agent_with_desc "$root" "my-agent" "$(desc_of_length 251)"
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_success
|
||||
assert_output --partial "SUGGESTION"
|
||||
@@ -654,7 +672,7 @@ EOF
|
||||
|
||||
@test "ADR-0020: agent description of exactly 400 chars is a SUGGESTION, not a FAIL" {
|
||||
local root="$TMPDIR/pkg"
|
||||
make_apm_agent_with_desc "$root" "my-agent" "$(python3 -c "print('x' * 400)")"
|
||||
make_apm_agent_with_desc "$root" "my-agent" "$(desc_of_length 400)"
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_success
|
||||
assert_output --partial "SUGGESTION"
|
||||
@@ -662,7 +680,7 @@ EOF
|
||||
|
||||
@test "ADR-0020: agent description of 401 chars FAILs and exits non-zero" {
|
||||
local root="$TMPDIR/pkg"
|
||||
make_apm_agent_with_desc "$root" "my-agent" "$(python3 -c "print('x' * 401)")"
|
||||
make_apm_agent_with_desc "$root" "my-agent" "$(desc_of_length 401)"
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_failure
|
||||
assert_output --partial "description is 401 chars"
|
||||
@@ -732,10 +750,18 @@ EOF
|
||||
# body becomes the system prompt of a fresh context. ADR-0020 gates the
|
||||
# former at 900 words and explicitly declines to gate the latter. If a body
|
||||
# word gate is ever added here, it contradicts the ADR.
|
||||
#
|
||||
# The description carries a boundary clause so the ONLY thing this test can
|
||||
# go red on is a body finding. Without one, the missing-boundary-clause
|
||||
# SUGGESTION fires and the blanket `refute_output --partial "SUGGESTION"`
|
||||
# below trips for a reason that has nothing to do with body length — which
|
||||
# would look like the invariant breaking while proving nothing about it.
|
||||
# AGENTS.md cites this test as the pin for that invariant, so it has to fail
|
||||
# for one reason and one reason only.
|
||||
{
|
||||
echo "---"
|
||||
echo "name: my-agent"
|
||||
echo "description: A valid agent description."
|
||||
echo "description: A valid agent description. Do not use for anything else."
|
||||
echo "---"
|
||||
echo ""
|
||||
python3 -c "print(' '.join(['word'] * 1500))"
|
||||
@@ -743,7 +769,13 @@ EOF
|
||||
run bash "$SCRIPT" "$root/.apm/agents/my-agent.agent.md"
|
||||
assert_success
|
||||
refute_output --partial "FAIL"
|
||||
# A 1,500-word body is 667% of the skill ceiling. Nothing may be said about
|
||||
# it at any tier: not a FAIL, not a SUGGESTION, and not the word-count
|
||||
# wording either tier would use if a gate were quietly added later.
|
||||
refute_output --partial "SUGGESTION"
|
||||
refute_output --partial "1500 words"
|
||||
refute_output --partial "900-word"
|
||||
refute_output --partial "body is"
|
||||
}
|
||||
|
||||
@test "a bare plugin.json with no apm.yml is no longer plugin scope — falls through to project scope" {
|
||||
@@ -778,3 +810,136 @@ EOF
|
||||
assert_success
|
||||
refute_output --partial "hooks"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# tools: — both YAML spellings
|
||||
# ---------------------------------------------------------------------------
|
||||
# The subagent-unavailable-tool SUGGESTION is read off the `tools` field, and
|
||||
# `tools` has two legal spellings: an inline scalar and a block sequence. The
|
||||
# field used to be pulled out with a line regex whose capture is newline-bounded
|
||||
# on purpose, so a block sequence captured NOTHING and the check silently
|
||||
# stopped firing — on the shape Copilot agent files actually use, which is to say
|
||||
# on the files it was written for. Both spellings are pinned, and they are pinned
|
||||
# together: the inline case alone was green throughout.
|
||||
|
||||
# make_pair <root> <tools-frontmatter> — a project-scope CC + Copilot pair
|
||||
# carrying the same `tools` value in both files. `tools` is on neither the
|
||||
# claude-code-only nor the copilot-only list, so it is legal in both and the pair
|
||||
# stays otherwise clean; the description carries a boundary clause so the only
|
||||
# SUGGESTION that can fire is the one under test.
|
||||
make_tools_pair() {
|
||||
local root="$1" tools="$2"
|
||||
mkdir -p "$root/.git" "$root/.claude/agents" "$root/.github/agents"
|
||||
local f
|
||||
for f in "$root/.claude/agents/my-agent.md" "$root/.github/agents/my-agent.agent.md"; do
|
||||
{
|
||||
echo "---"
|
||||
echo "name: my-agent"
|
||||
echo "description: A valid agent description. Do not use for anything else."
|
||||
echo "$tools"
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "You are a test agent. When invoked, do the thing."
|
||||
} > "$f"
|
||||
done
|
||||
}
|
||||
|
||||
@test "a subagent-unavailable tool in an INLINE tools scalar raises a SUGGESTION" {
|
||||
make_tools_pair "$TMPDIR/inline" "tools: Read ExitPlanMode"
|
||||
run bash "$SCRIPT" "$TMPDIR/inline/.claude/agents/my-agent.md"
|
||||
assert_success
|
||||
assert_output --partial "'ExitPlanMode' is listed in tools but is never available to subagents"
|
||||
}
|
||||
|
||||
@test "a subagent-unavailable tool in a BLOCK SEQUENCE tools field raises the same SUGGESTION" {
|
||||
make_tools_pair "$TMPDIR/block" "$(printf 'tools:\n - Read\n - ExitPlanMode')"
|
||||
run bash "$SCRIPT" "$TMPDIR/block/.claude/agents/my-agent.md"
|
||||
assert_success
|
||||
assert_output --partial "'ExitPlanMode' is listed in tools but is never available to subagents"
|
||||
}
|
||||
|
||||
@test "a tools list with no subagent-unavailable tool stays silent in both spellings" {
|
||||
# The control. Without it both cases above are satisfied by a check that
|
||||
# fires on every tools field it can see, which would be the opposite defect.
|
||||
make_tools_pair "$TMPDIR/inline-clean" "tools: Read Edit"
|
||||
run bash "$SCRIPT" "$TMPDIR/inline-clean/.claude/agents/my-agent.md"
|
||||
assert_success
|
||||
refute_output --partial "never available to subagents"
|
||||
|
||||
make_tools_pair "$TMPDIR/block-clean" "$(printf 'tools:\n - Read\n - Edit')"
|
||||
run bash "$SCRIPT" "$TMPDIR/block-clean/.claude/agents/my-agent.md"
|
||||
assert_success
|
||||
refute_output --partial "never available to subagents"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# A file that cannot be read
|
||||
# ---------------------------------------------------------------------------
|
||||
# scripts/check-apm-agents-valid.sh derives its expected agent-file set from
|
||||
# `git ls-files`, so it hands this script paths that are tracked but absent from
|
||||
# the worktree — a real and expected state, not a corner case. That used to exit
|
||||
# 1 with a bare FileNotFoundError traceback and no FAIL line at all: non-zero, so
|
||||
# the gate blocked, but with an interpreter stack instead of a diagnostic naming
|
||||
# the file. Both scope paths are covered because they are separate call sites
|
||||
# (check_apm_agent_file and check_file) and each needed its own handler.
|
||||
#
|
||||
# `is-a-dir.agent.md` is a DIRECTORY rather than a chmod 000 file on purpose:
|
||||
# these tests run as root in CI, where mode bits do not deny anything and a
|
||||
# permissions fixture would be silently readable and prove nothing.
|
||||
|
||||
@test "a nonexistent plugin/APM-scope agent file gets a FAIL naming the path, not a traceback" {
|
||||
local root="$TMPDIR/pkg"
|
||||
mkdir -p "$root/.apm/agents"
|
||||
cat > "$root/apm.yml" <<EOF
|
||||
name: test-package
|
||||
version: 0.1.0
|
||||
type: skill
|
||||
EOF
|
||||
run bash "$SCRIPT" "$root/.apm/agents/absent.agent.md"
|
||||
assert_failure
|
||||
assert_output --partial "FAIL"
|
||||
assert_output --partial "could not be read"
|
||||
assert_output --partial "absent.agent.md"
|
||||
refute_output --partial "Traceback"
|
||||
refute_output --partial "FileNotFoundError"
|
||||
}
|
||||
|
||||
@test "an unreadable plugin/APM-scope agent file gets a FAIL naming the path, not a traceback" {
|
||||
local root="$TMPDIR/pkg-dir"
|
||||
mkdir -p "$root/.apm/agents/is-a-dir.agent.md"
|
||||
cat > "$root/apm.yml" <<EOF
|
||||
name: test-package
|
||||
version: 0.1.0
|
||||
type: skill
|
||||
EOF
|
||||
run bash "$SCRIPT" "$root/.apm/agents/is-a-dir.agent.md"
|
||||
assert_failure
|
||||
assert_output --partial "FAIL"
|
||||
assert_output --partial "could not be read"
|
||||
assert_output --partial "is-a-dir.agent.md"
|
||||
refute_output --partial "Traceback"
|
||||
refute_output --partial "IsADirectoryError"
|
||||
}
|
||||
|
||||
@test "a nonexistent project-scope agent file gets a FAIL naming the path, not a traceback" {
|
||||
# The counterpart is pre-checked before either file is opened, so this
|
||||
# exercises the OTHER call site: the counterpart exists, the named file does
|
||||
# not, and check_file is what has to report it.
|
||||
local root="$TMPDIR/proj-missing"
|
||||
mkdir -p "$root/.git" "$root/.claude/agents" "$root/.github/agents"
|
||||
cat > "$root/.github/agents/my-agent.agent.md" <<EOF
|
||||
---
|
||||
name: my-agent
|
||||
description: A valid agent description. Do not use for anything else.
|
||||
---
|
||||
|
||||
You are a test agent. When invoked, do the thing.
|
||||
EOF
|
||||
run bash "$SCRIPT" "$root/.claude/agents/my-agent.md"
|
||||
assert_failure
|
||||
assert_output --partial "FAIL"
|
||||
assert_output --partial "could not be read"
|
||||
assert_output --partial "my-agent.md"
|
||||
refute_output --partial "Traceback"
|
||||
refute_output --partial "FileNotFoundError"
|
||||
}
|
||||
@@ -53,6 +53,8 @@ Gates `agent-audit` enforces at every scope:
|
||||
- **Body** — no word gate, and a delegation check in its place: name the skill to invoke rather than restating what it does.
|
||||
- **Invocation** — decide whether the agent is model-delegated or reached only by name. Only Copilot's cloud/IDE format expresses that in frontmatter (`disable-model-invocation`, `user-invocable`).
|
||||
|
||||
At every scope, five tools reach no subagent whatever `tools` says — `AskUserQuestion`, `EnterPlanMode`, `ExitPlanMode`, `ScheduleWakeup`, `WaitForMcpServers`. Never write a body that has the agent ask the user a question or enter plan mode; it describes a turn the runtime cannot give it.
|
||||
|
||||
## Step 4 — Validate and close
|
||||
|
||||
Invoke `agent-audit` on each file written and resolve every FAIL before reporting done. It checks the field allowlist, name-to-stem match, leftover placeholders and template comments, the description budget and the Copilot body limit — do not hand-check those.
|
||||
|
||||
@@ -34,10 +34,13 @@ description: FILL IN: Use when <trigger>. <One capability clause.> Not <thing> -
|
||||
boundary clause naming a real sibling skill or agent.
|
||||
250 characters is the target, 400 the hard ceiling (ADR-0020).
|
||||
Do not open with an action verb ("Reviews...", "Analyzes...") — that rule was
|
||||
deleted. Add "Use proactively" only if the runtime should delegate here without
|
||||
the user naming this agent.
|
||||
deleted.
|
||||
Never write "Use proactively" here. It steers the Claude Code runtime and does
|
||||
nothing anywhere else, and this file compiles to a Copilot `.agent.md` too, where
|
||||
agent-audit's KyberforgeCopilot.ProactivePhrase rule grades it a hard FAIL.
|
||||
The phrase is CC-only; at this scope, a precise trigger clause does that job.
|
||||
Example: "Use when a diff needs checking for injected credentials before it
|
||||
merges. Not general code review -> code-reviewer." -->
|
||||
merges. Not prose or style linting -> `lint-runner`." -->
|
||||
|
||||
<!-- model: sonnet
|
||||
Optional. Aliases: sonnet, opus, haiku, fable. Or full model ID.
|
||||
@@ -80,3 +83,10 @@ FILL IN: Steps the agent takes. Be specific about ordering if it matters.
|
||||
## Output
|
||||
|
||||
FILL IN: What does the agent produce? Format, location, structure.
|
||||
|
||||
## Errors
|
||||
|
||||
FILL IN: What does the agent do on malformed, missing or contradictory input?
|
||||
State whether it stops and reports, or degrades to a named fallback — and what it
|
||||
tells the caller either way. An agent with no error handling invents a recovery,
|
||||
and an invented recovery is invisible until the output is wrong.
|
||||
@@ -14,19 +14,24 @@ description: FILL IN: Use when <trigger>. <One capability clause.> Not <thing> -
|
||||
boundary clause naming a real sibling skill or agent.
|
||||
250 characters is the target, 400 the hard ceiling (ADR-0020).
|
||||
Do not open with an action verb ("Reviews...", "Analyzes...") — that rule was
|
||||
deleted. Add "Use proactively" only if the runtime should delegate here without
|
||||
the user naming this agent.
|
||||
deleted.
|
||||
"Use proactively" is valid HERE and only here: it steers the Claude Code runtime
|
||||
to offer this agent unprompted. Add it only if that is what you want. If you add
|
||||
it, leave it OUT of the Copilot half of the pair — the phrase does nothing there
|
||||
and agent-audit's KyberforgeCopilot.ProactivePhrase grades it a hard FAIL. The
|
||||
pair must describe the same job; it does not have to be byte-identical.
|
||||
Example: "Use when a diff needs checking for injected credentials before it
|
||||
merges. Not general code review -> code-reviewer." -->
|
||||
merges. Not prose or style linting -> `lint-runner`." -->
|
||||
|
||||
<!-- tools: Read, Bash, Grep
|
||||
Optional. Allowlist of tool names: a comma-separated string or a YAML list.
|
||||
Omit to inherit all tools from parent.
|
||||
Restrict it to what the agent actually needs. Omit only when it needs them
|
||||
all — omitting inherits every tool from the parent.
|
||||
Use Agent(type1,type2) to restrict which subagent types this agent can spawn.
|
||||
Omit Agent entirely to prevent this agent from spawning subagents.
|
||||
Never available to subagents regardless of tools field:
|
||||
AskUserQuestion, EnterPlanMode, ExitPlanMode, ScheduleWakeup, WaitForMcpServers
|
||||
Exception: ExitPlanMode IS available when parent session runs in permissionMode: plan -->
|
||||
Listing any of them is a finding: agent-audit enforces the flat rule. -->
|
||||
|
||||
<!-- model: sonnet
|
||||
Optional. Aliases: sonnet, opus, haiku, fable. Or full model ID.
|
||||
@@ -99,3 +104,10 @@ FILL IN: Steps the agent takes. Be specific about ordering if it matters.
|
||||
## Output
|
||||
|
||||
FILL IN: What does the agent produce? Format, location, structure.
|
||||
|
||||
## Errors
|
||||
|
||||
FILL IN: What does the agent do on malformed, missing or contradictory input?
|
||||
State whether it stops and reports, or degrades to a named fallback — and what it
|
||||
tells the caller either way. An agent with no error handling invents a recovery,
|
||||
and an invented recovery is invisible until the output is wrong.
|
||||
+13
-2
@@ -19,9 +19,13 @@ description: FILL IN: Use when <trigger>. <One capability clause.> Not <thing> -
|
||||
boundary clause naming a real sibling skill or agent.
|
||||
250 characters is the target, 400 the hard ceiling (ADR-0020).
|
||||
Do not open with an action verb ("Reviews...", "Analyzes...") — that rule was deleted.
|
||||
Keep it identical in wording to the Claude Code half of the pair.
|
||||
Never write "Use proactively" here. It steers the Claude Code runtime and does nothing
|
||||
in Copilot, and agent-audit's KyberforgeCopilot.ProactivePhrase grades it a hard FAIL.
|
||||
Otherwise keep the wording matched to the Claude Code half of the pair: agent-audit
|
||||
checks that both halves describe the same job, not that they are byte-identical, so
|
||||
dropping the CC-only phrase here is not a pair-consistency finding.
|
||||
Example: "Use when a diff needs checking for injected credentials before it
|
||||
merges. Not general code review -> code-reviewer." -->
|
||||
merges. Not prose or style linting -> `lint-runner`." -->
|
||||
|
||||
<!-- tools: ["read", "search", "edit"]
|
||||
Optional. Array of tool names. Omit = all available tools. [] = no tools.
|
||||
@@ -68,3 +72,10 @@ FILL IN: Steps the agent takes. Be specific about ordering if it matters.
|
||||
## Output
|
||||
|
||||
FILL IN: What does the agent produce? Format, location, structure.
|
||||
|
||||
## Errors
|
||||
|
||||
FILL IN: What does the agent do on malformed, missing or contradictory input?
|
||||
State whether it stops and reports, or degrades to a named fallback — and what it
|
||||
tells the caller either way. An agent with no error handling invents a recovery,
|
||||
and an invented recovery is invisible until the output is wrong.
|
||||
@@ -44,15 +44,37 @@ Banned from a description; move it to the body or to `README.md`:
|
||||
and ADR-0020 deleted it: the opener is `Use when`, matching every skill in this corpus, so one
|
||||
router reads one shape.
|
||||
|
||||
**"Use proactively" is conditional.** Add it only where the runtime should delegate without the
|
||||
user naming the agent — an agent invoked by name does not need it, and it costs activations
|
||||
elsewhere when added by reflex. The same conditional governs indirect triggers ("even if the user
|
||||
doesn't say X"): add one only where the user's natural phrasing genuinely omits the domain word.
|
||||
**"Use proactively" is Claude Code-only, and conditional even there.** The phrase steers the
|
||||
Claude Code runtime to offer an agent unprompted and does nothing anywhere else, so where it may
|
||||
appear depends on the file:
|
||||
|
||||
**Boundary targets must resolve.** The name after the arrow is checked against real skills under
|
||||
`plugins/*/.apm/skills/<name>/` and real agents under `plugins/*/.apm/agents/<name>.agent.md`. A
|
||||
target that does not exist sends the router nowhere. Verify it before writing it — do not invent a
|
||||
plausible sibling.
|
||||
| File | Rule |
|
||||
|---|---|
|
||||
| Claude Code `.md` (project/user scope) | Allowed. Add it only where the runtime should delegate without the user naming the agent — an agent invoked by name does not need it, and it costs activations elsewhere when added by reflex. |
|
||||
| Copilot `.agent.md` (project/user scope) | **Never.** Inert there, and `KyberforgeCopilot.ProactivePhrase` grades it a hard FAIL. |
|
||||
| Vendor-neutral `.apm/agents/<name>.agent.md` (plugin/APM scope) | **Never.** Same Vale rule, same hard FAIL — the file matches the `**/*.agent.md` glob, and it compiles to a real Copilot agent downstream. |
|
||||
|
||||
A pair whose Claude Code half carries the phrase and whose Copilot half omits it is correct, not
|
||||
inconsistent: `agent-audit` checks that both halves describe the same job, not that they match
|
||||
word for word.
|
||||
|
||||
Indirect triggers ("even if the user doesn't say X") take a similar conditional at every scope:
|
||||
add one only where the user's natural phrasing genuinely omits the domain word.
|
||||
|
||||
**Boundary targets must resolve.** Both forms are checked — the arrow and the prose form ("do not
|
||||
use for X, use `y` instead") — so a typo dangles either way. Targets resolve against a universe
|
||||
built by walking up **from the agent file itself**: the nearest ancestor holding
|
||||
`plugins/*/.apm/{skills,agents}` (or, failing that, the nearest ancestor holding `.git`) contributes
|
||||
every skill and agent under `<root>/plugins/*/`, plus the agent's own apm package and the packages
|
||||
that package declares in `apm.yml` under `dependencies.apm`. A sibling plugin in the same monorepo
|
||||
therefore resolves; a skill in an unrelated repo does not. A target outside that universe sends the
|
||||
router nowhere. Verify it before writing it — do not invent a plausible sibling.
|
||||
|
||||
That universe is the apm marketplace and stops there. A **host built-in is not a routing target**:
|
||||
`/compact`, `/clear` and `/init` are Claude Code slash commands with no counterpart in Copilot CLI
|
||||
or Codex, and `.apm/` source compiles for all three, so routing to one is a portability defect. The
|
||||
gate is right to fail it and there is no allowlist. If a built-in genuinely needs mentioning, write
|
||||
it un-slashed — ``the `compact` built-in`` — which makes no routing claim and is not checked.
|
||||
|
||||
**Length.** 250 characters SUGGESTION, 400 characters FAIL, counting the frontmatter value only
|
||||
with YAML folding resolved. Treat 250 as the target: the SUGGESTION tier is what moves the corpus
|
||||
@@ -73,8 +95,18 @@ You are a <role>. When invoked, <primary action>.
|
||||
|
||||
## Output
|
||||
<what it produces: format, location, structure>
|
||||
|
||||
## Errors
|
||||
<what to do on malformed, missing or contradictory input: report and stop, or
|
||||
which fallback to take — and what to say to the caller either way>
|
||||
````
|
||||
|
||||
Four required elements: **inputs expected, process steps, output format, error handling.** The
|
||||
last is the one that gets dropped, and dropping it is not neutral: an agent given a malformed
|
||||
input and no instruction invents a recovery, and a subagent's invented recovery is invisible to
|
||||
the caller until the output is wrong. Say explicitly whether the agent stops and reports, or
|
||||
degrades to a named fallback.
|
||||
|
||||
One job per agent. An agent covering two jobs gets delegated to for the wrong one.
|
||||
|
||||
**Delegation discipline replaces the word gate.** A plugin/APM agent is a single file with no
|
||||
|
||||
@@ -57,6 +57,11 @@ procedure a skill it can invoke already owns is an `agent-audit` FAIL. When a si
|
||||
missing procedure, check first whether an installed skill owns it and name that skill instead of
|
||||
transcribing it. See `references/contract.md`.
|
||||
|
||||
The delegation check is not a length brake — it fires only on procedure an invocable skill already
|
||||
owns, and says nothing about original prose. That brake is judgment, and it is the only one left:
|
||||
for every sentence you add, ask "would the agent get this wrong without it?" and delete it if the
|
||||
answer is no.
|
||||
|
||||
**Explain the why.** Reasoning-based instructions outperform rigid directives. A rule written in
|
||||
all caps (ALWAYS/NEVER) is usually better reframed as why the behaviour matters, so the agent can
|
||||
apply judgment at the edges.
|
||||
@@ -73,4 +78,10 @@ that was already there.
|
||||
If the edit adds or removes research-sourced content, update `source_keys` in the edited file and
|
||||
the matching `sources.md` entry — the create flow's Step 3 has the rules.
|
||||
|
||||
**Check for regressions before handing back.** `SKILL.md` Step 4 tells you to resolve every FAIL,
|
||||
which says nothing about a check that passed *before* these edits and no longer does. Compare the
|
||||
closing `agent-audit` against the agent's pre-edit state — a PASS that has become a SUGGESTION, or
|
||||
a SUGGESTION that has become a FAIL, is damage this flow caused and is in scope for it. Only the
|
||||
improve flow can make that comparison; the create flow has no prior state to compare against.
|
||||
|
||||
Then return to `SKILL.md` Step 4.
|
||||
@@ -37,6 +37,11 @@ The rule is about a field's *shape*, not a fixed roster:
|
||||
Claude Code honours it for plugin subagents; the three fields plugin agents do silently ignore
|
||||
are `hooks`, `mcpServers` and `permissionMode`, and this is not one of them. Copilot's handling
|
||||
of the key is unconfirmed, which ADR-0016 accepts as a stated risk.
|
||||
|
||||
Its syntax is the same at every scope, and this is the one scope that cannot reach it anywhere
|
||||
else: MCP tools are denied as `mcp__<server>`, `mcp__<server>__*` or `mcp__*`; both a YAML list
|
||||
and a delimited string are accepted, and this repo writes the comma-separated string form
|
||||
(`disallowedTools: Edit, Write, NotebookEdit`) — match it.
|
||||
- The Claude-only knobs (`isolation`, `maxTurns`, `effort`, `memory`, `permissionMode`, `skills`,
|
||||
`color`, `initialPrompt`, `background`, `hooks`, `mcpServers`) have no Copilot equivalent and
|
||||
are never written to this file at all. "Silently ignored at plugin scope" is the wrong framing:
|
||||
@@ -63,5 +68,7 @@ to installed skills instead of transcribed procedure.
|
||||
- [ ] No `FILL IN:` placeholder and no `<!-- ... -->` template comment anywhere in the file
|
||||
- [ ] System prompt body non-empty, and a read-only agent says so in prose as well as in
|
||||
`disallowedTools`
|
||||
- [ ] Body covers all four required elements: inputs expected, process steps, output format,
|
||||
**error handling** — what the agent does on malformed, missing or contradictory input
|
||||
|
||||
Then return to the flow reference you came from.
|
||||
@@ -25,7 +25,10 @@ duplicate silently.
|
||||
**`description`** — write it against `references/contract.md`. It is the primary signal for
|
||||
autonomous delegation.
|
||||
|
||||
**`tools`** — an allowlist; omit it to inherit every tool from the parent. Use `Agent(type1,type2)`
|
||||
**`tools`** — an allowlist. Write it, and restrict it to the tools the agent actually needs;
|
||||
omitting it inherits every tool from the parent, which is the right value only when the agent
|
||||
genuinely needs all of them. Least privilege is the default, not the exception. Use
|
||||
`Agent(type1,type2)`
|
||||
to restrict which subagent types this agent may spawn, and omit `Agent` entirely to stop it
|
||||
spawning any. Five tools reach no subagent whatever this field says — `AskUserQuestion`,
|
||||
`EnterPlanMode`, `ExitPlanMode`, `ScheduleWakeup` and `WaitForMcpServers` — so listing one buys
|
||||
@@ -95,6 +98,8 @@ Both files:
|
||||
|
||||
- [ ] `name` present and kebab-case; `description` written to `references/contract.md`
|
||||
- [ ] System prompt body present, non-empty and equivalent across the pair
|
||||
- [ ] Body covers all four required elements: inputs expected, process steps, output format,
|
||||
**error handling** — what the agent does on malformed, missing or contradictory input
|
||||
- [ ] No `FILL IN:` placeholder and no `<!-- ... -->` template comment left
|
||||
|
||||
Copilot file only:
|
||||
|
||||
@@ -285,6 +285,8 @@ else
|
||||
if [[ "$SCOPE" == "plugin" ]]; then
|
||||
echo " 1. Fill in $APM_FILE — replace every FILL IN: placeholder. Optional fields are" >&2
|
||||
echo " scaffolded there as commented blocks; uncomment the ones that apply." >&2
|
||||
echo " Description: 250 chars target / 400 ceiling (ADR-0020). The body has no" >&2
|
||||
echo " word gate — delegate to a skill instead of restating what it does." >&2
|
||||
echo " 2. Populate $SOURCES_DIR/sources.md with research sources, or delete it" >&2
|
||||
echo " 3. Validate: $VALIDATE_HINT $APM_FILE" >&2
|
||||
echo " It checks the frontmatter against the apm-agent-allowlist section of" >&2
|
||||
@@ -292,6 +294,8 @@ else
|
||||
else
|
||||
echo " 1. Fill in $CC_FILE — replace every FILL IN: placeholder. Optional fields are" >&2
|
||||
echo " scaffolded there as commented blocks; uncomment the ones that apply." >&2
|
||||
echo " Description: 250 chars target / 400 ceiling (ADR-0020). The body has no" >&2
|
||||
echo " word gate — delegate to a skill instead of restating what it does." >&2
|
||||
echo " 2. Fill in $CP_FILE — same, and heed its closing comment: the Claude Code-only" >&2
|
||||
echo " fields it names must not cross over from the file above." >&2
|
||||
echo " 3. Validate: run $VALIDATE_HINT on each file" >&2
|
||||
|
||||
@@ -6,10 +6,12 @@ Audit a skill directory against the agentskills.io specification and the house c
|
||||
|
||||
1. Runs `scripts/validate.sh` and `scripts/validate-provenance.sh` for structural and provenance checks, plus `scripts/vale-wrap.sh` — a Vale prefilter that deterministically flags non-imperative description openers, composition and architecture notes, vague wording, padding phrases, and "There is/are" sentence openers
|
||||
2. Reads all files in the skill directory
|
||||
3. Applies qualitative checks across six dimension groups, loading one rubric from `references/` per group
|
||||
3. Applies qualitative checks across five dimension groups, loading one rubric from `references/` per group
|
||||
4. Outputs a compact findings report — findings only, grouped by dimension, each with Why and Fix — and a result block with handoff to `skill-author`
|
||||
|
||||
`validate.sh` enforces two independent length families that must not be conflated: the agentskills.io spec conformance ceilings (500 lines, 2,770 words, both counting the whole file) and the ADR-0020 context budget (250/400 description characters, 600/900 body-only words, plus resolvable boundary targets).
|
||||
`validate.sh` enforces two independent length families that must not be conflated: the agentskills.io spec conformance ceilings (500 lines, 2,770 words, both counting the whole file) and the ADR-0020 context budget (250/400 description characters, 600/900 body-only words).
|
||||
|
||||
Alongside those it runs four shape checks that are not length measurements at all. Two are FAILs: every routing target named in the description — in the compressed `Not <thing> -> <name>` arrow **and** in the prose form — must resolve to a real skill or agent, and every `references/<file>.md` the body names must exist on disk. Three are SUGGESTIONs: a missing boundary clause, a Gotchas section over five entries, and a Gotchas section over 25% of the body. The resolution universe for boundary targets is derived by walking up from the audited `SKILL.md` — the authoring root above it, its own apm package, and that package's declared `apm.yml` dependencies — so a fresh clone and a machine that has run `apm install` return the same verdict. When no universe can be determined the check prints `INFO ... DID NOT RUN` and does not silently pass.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -24,7 +26,7 @@ Provide the path to the skill directory to audit when invoking.
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `SKILL.md` | Skill instructions for agents |
|
||||
| `scripts/validate.sh` | Structural validator — checks name format, name matches directory, description length, line count, placeholder detection, script executable bit, and interactive-prompt detection |
|
||||
| `scripts/validate.sh` | Structural validator — checks name format, name matches directory, description presence and length, body-only word count, line and whole-file word ceilings, boundary-clause presence, boundary-target resolution, `references/` pointer existence, Gotchas entry count and body share, placeholder detection, script executable bit, and interactive-prompt detection |
|
||||
| `scripts/validate-provenance.sh` | Provenance validator — checks sources.md completeness, source_keys/slug consistency, Contributing files existence, bidirectional linkage, Research doc: fields, and upstream research doc alignment |
|
||||
| `scripts/vale-wrap.sh` | Vale prefilter wrapper — runs the bundled `Kyberforge` Vale styles against SKILL.md and reports alerts as deterministic FAILs ahead of Step 3's qualitative review |
|
||||
| `assets/vale/.vale.ini` | Vale configuration — points Vale at the bundled `Kyberforge` style path, self-located relative to `vale-wrap.sh` |
|
||||
@@ -38,6 +40,7 @@ Provide the path to the skill directory to audit when invoking.
|
||||
| `references/patterns.md` | Rubric for the patterns dimension — which instruction construct fits which job, and how each is correctly formed |
|
||||
| `references/file-structure.md` | Rubric for the file-structure and internal-consistency dimensions — permitted directories, cross-plugin path rules and their two structural exemptions, README drift |
|
||||
| `references/formatting-and-scripts.md` | Rubric for the formatting and scripts dimensions — heading and fencing conventions, and the agentic-use criteria for bundled scripts |
|
||||
| `references/validation-scripts.md` | Step 1 troubleshooting — the manual structural fallback when `validate.sh` cannot run, and the script exit codes that are easy to misread (loaded only on a script failure) |
|
||||
| `references/sources.md` | Provenance record — agentskills.io sources that informed this skill and which files each contributed to |
|
||||
| `tests/validate.bats` | (source-only) Bats test suite for validate.sh |
|
||||
| `tests/validate-provenance.bats` | (source-only) Bats test suite for validate-provenance.sh |
|
||||
|
||||
@@ -33,7 +33,9 @@ bash scripts/validate-provenance.sh <skill-dir>
|
||||
scripts/vale-wrap.sh <skill-dir>/SKILL.md
|
||||
```
|
||||
|
||||
`validate.sh` findings become the `### Structure` dimension — its FAILs and its SUGGESTIONs both. If it cannot run at all (no `python3`, Bash denied), report that as an INFO finding rather than guessing; what it measures is not reproducible by reading.
|
||||
`validate.sh` findings become the `### Structure` dimension — its FAILs and its SUGGESTIONs both.
|
||||
|
||||
If any of the three cannot run, or exits non-zero for a reason other than findings, read `references/validation-scripts.md` — it carries the manual fallback and the misleading exit codes. Ordinary content FAILs are the expected outcome here and need no fallback.
|
||||
|
||||
`validate-provenance.sh` prints nothing on success. Its FAIL and INFO findings become a separate `### Provenance` dimension, and it emits Why and Fix itself — surface those verbatim.
|
||||
|
||||
|
||||
@@ -28,8 +28,10 @@ Include content the agent lacks:
|
||||
- The specific tools or sequences to use — not the full range of options
|
||||
- One default per decision point with one escape hatch
|
||||
|
||||
Move to `references/`, behind an explicit "If X, read `references/file.md`" trigger — the literal
|
||||
conditional form, never a generic pointer:
|
||||
Move to `references/`, behind an explicit "If X, read `references/<file>.md`" trigger — the literal
|
||||
conditional form, never a generic pointer. Write the real filename in the skill under audit; the
|
||||
angle brackets are a placeholder here, and a literal `references/file.md` in a body is an ERROR
|
||||
from the ADR-0020 gate because no such file exists on disk. Move:
|
||||
|
||||
- Lookup tables and spec restatements
|
||||
- Output schemas, templates and example blocks
|
||||
@@ -69,8 +71,10 @@ table** plus the gates common to every branch, and each flow lives in its own se
|
||||
`references/` file. Inlining all of them is a FAIL regardless of word count, because every
|
||||
invocation then pays for every branch it did not take.
|
||||
|
||||
The reference shape in this repo is `apm-workflow`: a 554-word body dispatching to roughly 3,000
|
||||
words of references across five mutually exclusive invocations.
|
||||
The reference shape in this repo is `apm-workflow`: a **421-word body** dispatching to roughly
|
||||
3,000 words of references across five mutually exclusive invocations. Its whole-file count is 554
|
||||
words — cite 421 when calibrating a body, or the conflation this section warns against reappears
|
||||
in the finding itself.
|
||||
|
||||
## Gotchas sections
|
||||
|
||||
@@ -86,17 +90,20 @@ sensibly.
|
||||
|
||||
Constraints:
|
||||
|
||||
- **Maximum five entries.** Past five, the section is a summary of the body rather than a set of
|
||||
traps, and the agent stops reading it as a warning.
|
||||
- **More than five entries is a SUGGESTION** — five is the guideline, not a ceiling. Past five, the
|
||||
section is usually a summary of the body rather than a set of traps, and the agent stops reading
|
||||
it as a warning. It stays advisory because whether a given gotcha earns its place is judgment;
|
||||
`validate.sh` emits it through `suggest()` and the run still exits 0.
|
||||
- **A Gotcha that paraphrases a step in the body below it is a FAIL.** It has no independent
|
||||
content, and it teaches the agent that Gotchas can be skimmed because the real instruction is
|
||||
coming.
|
||||
coming. This one is the auditor's call — no script detects it.
|
||||
- **A Gotchas section exceeding 25% of the body is a SUGGESTION** — the body has been inverted into
|
||||
a preamble.
|
||||
a preamble. Same tier and same reasoning as the entry count, and independent of it: either can
|
||||
fire without the other.
|
||||
- Place the section near the top. A gotcha read after the mistake is worthless, which is also why
|
||||
Gotchas is the one construct exempt from moving to `references/`.
|
||||
|
||||
Worked negative example — `git-commits` carries thirteen entries, of which four restate content
|
||||
Worked negative example — `git-commits` carries twelve entries, of which four restate content
|
||||
that already appears below or in the description:
|
||||
|
||||
| Gotcha | Restates |
|
||||
@@ -106,8 +113,10 @@ that already appears below or in the description:
|
||||
| `:33` "Never skip hooks with `--no-verify`" | step 9 at `:52` |
|
||||
| `:36` "Never commit secrets" | step 2 at `:45` |
|
||||
|
||||
All four are FAILs under this rule, and the section as a whole breaches the five-entry maximum. It
|
||||
also passes every plausible word gate, which is the point of auditing the construct directly.
|
||||
All four are FAILs under the paraphrase rule. The entry count and the section's share of the body
|
||||
(387 of 1,102 words, 35%) are two further SUGGESTIONs on top — the script reports both, and neither
|
||||
fails the run on its own. What makes this worth auditing directly is that the four paraphrase FAILs
|
||||
pass every word gate there is; only reading the construct finds them.
|
||||
|
||||
## Calibrating control
|
||||
|
||||
@@ -144,7 +153,7 @@ Flag as FAIL if:
|
||||
- A sentence answers "no" to the core test — it is padding
|
||||
- The body exceeds 900 words counted body-only (`validate.sh` reports it)
|
||||
- Two or more mutually exclusive flows are inlined instead of dispatched
|
||||
- A Gotcha paraphrases a step in the body below it, or the section exceeds five entries
|
||||
- A Gotcha paraphrases a step in the body below it
|
||||
- A decision point presents a menu of options with no default
|
||||
- An instruction repeats content already in the description
|
||||
- A prescriptive sequence is used where flexibility is fine, or the reverse
|
||||
@@ -152,6 +161,7 @@ Flag as FAIL if:
|
||||
Flag as SUGGESTION if:
|
||||
|
||||
- The body exceeds 600 words counted body-only but stays at or under 900
|
||||
- The Gotchas section carries more than five entries
|
||||
- The Gotchas section exceeds 25% of the body
|
||||
- A rationale is missing from an include/exclude rule — present but unexplained
|
||||
- Gotchas are correct but placed late in the body rather than near the top
|
||||
|
||||
@@ -28,6 +28,14 @@ resolving there. Flag any `../`, `../../`, or absolute repo path (`plugins/<plug
|
||||
and its APM-native equivalent `.apm/skills/<other>/`) appearing in `SKILL.md`, `scripts/`,
|
||||
`references/` or `assets/`.
|
||||
|
||||
**Referring to another skill's file.** There is one sanctioned spelling, and it is possessive:
|
||||
`skill-audit's references/validation-scripts.md`. Write the skill by name and let the reader
|
||||
resolve it — do not spell the repo path. The full path is the thing this section forbids, and
|
||||
`references/validation-scripts.md` on its own is a hard ERROR from the ADR-0020 gate, which
|
||||
requires an unqualified `references/` pointer to exist in the skill's OWN directory. The
|
||||
possessive form is the only spelling both rules accept; the gate recognises it and skips the
|
||||
on-disk check. Flag any other spelling of a cross-skill reference.
|
||||
|
||||
Two directories are exempt, and the exemptions are structural rather than discretionary:
|
||||
|
||||
- **`references/sources.md`.** Its `Research doc:` fields are development-time provenance pointers,
|
||||
|
||||
@@ -32,8 +32,16 @@ read after the mistake.
|
||||
inner fence as `` \`\`\` ``. An unescaped inner fence terminates the outer block and the remaining
|
||||
instructions render as prose.
|
||||
|
||||
**Conditional references** state a specific trigger: "If the API returns a non-200 status, read
|
||||
`references/api-errors.md`." The generic form — pointing at the directory and hoping — defeats
|
||||
**Conditional references** state a specific trigger, naming a file that exists in the skill's own
|
||||
`references/` directory:
|
||||
|
||||
```text
|
||||
If the API returns a non-200 status, read `references/api-errors.md`.
|
||||
```
|
||||
|
||||
That block is fenced because the filename in it is illustrative — an unfenced `references/` pointer
|
||||
in a `SKILL.md` body must resolve on disk or the ADR-0020 gate reports a hard ERROR. The generic
|
||||
form — pointing at the directory and hoping — defeats
|
||||
progressive disclosure, because the agent either loads everything or loads nothing.
|
||||
`Kyberforge.PaddingPhrase` catches the common generic phrasing deterministically; other malformed
|
||||
forms are judgment.
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
- **URL:** https://agentskills.io/specification.md
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
|
||||
- **Description:** Complete SKILL.md format specification — frontmatter fields, constraints, body content, optional directories, progressive disclosure levels, file references, validation
|
||||
- **Contributing files:** SKILL.md, references/body-discipline.md, references/description-quality.md, references/patterns.md, references/file-structure.md, references/formatting-and-scripts.md
|
||||
- **Contributing files:** SKILL.md, references/body-discipline.md, references/description-quality.md, references/patterns.md, references/file-structure.md, references/formatting-and-scripts.md, references/validation-scripts.md
|
||||
- **Status:** `extracted`
|
||||
|
||||
## agentskills-best-practices
|
||||
@@ -47,7 +47,7 @@
|
||||
- **URL:** https://agentskills.io/skill-creation/using-scripts.md
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
|
||||
- **Description:** Using scripts in skills — one-off commands, self-contained scripts with inline dependencies, designing scripts for agentic use (no interactive prompts, --help, structured output, idempotency)
|
||||
- **Contributing files:** SKILL.md, references/formatting-and-scripts.md
|
||||
- **Contributing files:** SKILL.md, references/formatting-and-scripts.md, references/validation-scripts.md
|
||||
- **Status:** `extracted`
|
||||
|
||||
## agentskills-quickstart
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
source_keys:
|
||||
- agentskills-spec
|
||||
- agentskills-using-scripts
|
||||
---
|
||||
|
||||
# Validation Scripts Reference
|
||||
|
||||
Read this when a Step 1 script fails, cannot run, or reports something that needs interpreting.
|
||||
Nothing here is needed on a clean run.
|
||||
|
||||
## Report the gap, do not guess
|
||||
|
||||
If a script cannot run at all — Bash denied, `python3` unavailable, PyYAML not importable, `vale`
|
||||
not installed — say so as an **INFO** finding naming the script and the missing dependency, then
|
||||
fall back to the manual checks below. An INFO never changes PASS/FAIL. Silently omitting the
|
||||
dimension a script would have covered reports a clean audit that checked less than it claims to
|
||||
have checked, and the Step 4 coverage line then names a dimension nothing actually examined.
|
||||
|
||||
## Manual structural fallback
|
||||
|
||||
`validate.sh` needs `python3` **and** PyYAML, and refuses to start without either — the description
|
||||
value has to be measured after YAML folding is resolved, so skipping the ADR-0020 gates would be a
|
||||
vacuous pass rather than a partial one. The two are checked separately, so the message already names
|
||||
the right one — report it verbatim rather than diagnosing further:
|
||||
|
||||
```text
|
||||
Error: python3 is required but was not found on PATH.
|
||||
Error: PyYAML is required but is not importable by python3.
|
||||
```
|
||||
|
||||
Without them — or with Bash denied, or on a permission error — work this list
|
||||
by hand and file the results under `### Structure` exactly as the script's output would have been:
|
||||
|
||||
- **`name`** present, 1–64 characters, kebab-case (lowercase letters, digits and hyphens; no
|
||||
leading, trailing or doubled hyphen), and **matching the skill's directory name** exactly.
|
||||
- **`description`** present and non-empty; no unfilled `FILL IN:` placeholder in it. An absent or
|
||||
empty description is a **FAIL**, never a silent skip — it is the one field preloaded into every
|
||||
session, so a skill without one can never be routed to.
|
||||
- **Description length**, measured on the folded YAML value with newlines collapsed to single
|
||||
spaces — not on the raw block scalar, which counts indentation. 250 characters SUGGESTION, 400
|
||||
FAIL (ADR-0020), 1,024 FAIL (agentskills.io spec).
|
||||
- **Body length**, counting everything after the frontmatter's closing `---`. 600 words
|
||||
SUGGESTION, 900 FAIL (ADR-0020).
|
||||
- **Whole-file ceilings**, counting the file including frontmatter: 500 lines FAIL, 2,770 words
|
||||
FAIL (agentskills.io spec). These are a different measurement from the two above — report them
|
||||
as separate findings, never merged.
|
||||
- **A boundary clause is present** — either the prose form (`do not` / `instead` / `rather than` /
|
||||
`not for`) or ADR-0020's compressed `Not <thing> -> <name>` arrow. **SUGGESTION**, not FAIL:
|
||||
the absence is deterministic, but whether this skill warrants one is the auditor's call.
|
||||
- **Boundary targets resolve** — **FAIL** on a name that resolves to nothing. See the section
|
||||
below; resolving these by hand is the one item on this list with a procedure of its own.
|
||||
- **Every `references/<file>.md` named in the body exists on disk** — **FAIL**, not a suggestion.
|
||||
A dispatch table or "read X" trigger naming a missing file sends the agent nowhere. Ignore
|
||||
mentions inside fenced code blocks, and ignore a mention whose own line says the file is gone
|
||||
(`removed`, `deleted`, `renamed`, `superseded`, `replaced`, `obsolete`, `deprecated`, `former`,
|
||||
`gone`, `no longer`, `used to`) — that is a historical note, not a dispatch entry.
|
||||
- **Gotchas discipline**, both **SUGGESTION**. Locate the section by a heading that *is* Gotchas
|
||||
(`## Common Gotchas` counts; `## Gotcha handling` and `## Why gotchas matter` do not), running to
|
||||
the next heading at the same level or shallower. More than five top-level entries is one
|
||||
suggestion; a section over 25% of the body word count is a second, independent one. Count
|
||||
entries at column 0 only — an indented child bullet is not an entry — and ignore fenced code
|
||||
blocks for both.
|
||||
- **No unfilled `FILL IN:` placeholder** anywhere in the body.
|
||||
- **Every file in `scripts/`** carries the executable bit and contains no interactive prompt —
|
||||
no bare `read`, no `select`, nothing that blocks on a TTY.
|
||||
|
||||
## Resolving boundary targets by hand
|
||||
|
||||
Targets are read from **both** boundary forms. The compressed `Not <thing> -> <name>` arrow and the
|
||||
prose form are each parsed *and* target-checked, so a typo in prose phrasing fails exactly as an
|
||||
arrow typo does — do not check only the names after an arrow.
|
||||
|
||||
Build the universe by walking up **from the `SKILL.md` under audit**, never from the validator's own
|
||||
location. The nearest ancestor holding `plugins/*/.apm/skills/` or `plugins/*/.apm/agents/` is the
|
||||
authoring root, falling back to the nearest ancestor holding `.git`. When one is found the universe
|
||||
is every skill and agent under `<root>/plugins/*/`, plus the skill's own apm package, plus the
|
||||
packages that package declares in its `apm.yml` under `dependencies.apm`. Deployed `.claude/` and
|
||||
`.agents/` trees are consulted **only** when no authoring root exists — they are gitignored
|
||||
`apm install` output, and reading them would make a fresh clone and a developer machine disagree.
|
||||
|
||||
Three ways to read the result wrong:
|
||||
|
||||
- **A hyphenated name used attributively is not a dangling target.** "Use pre-commit hooks instead
|
||||
of ad-hoc scripts" reads as a route to `pre-commit` on wording alone. What separates a route from
|
||||
prose is grammar: a route target is terminal — followed by punctuation, a conjunction, or a
|
||||
boundary word — whereas a compound modifier is followed by the noun it modifies. A name followed
|
||||
by an ordinary noun still *confirms* a route when it exists, but never raises a FAIL on its own.
|
||||
- **A SUGGESTION-tier unresolved target is not a FAIL you may promote.** Terminal position alone is
|
||||
not evidence of a route: "run `pre-commit` instead", "see `commit-msg`" and "use the clean-up
|
||||
instead" are all terminal and all prose. A prose-form target earns a FAIL only when its own
|
||||
sentence names another target that *does* resolve; otherwise the script reports it and moves on,
|
||||
and so should you. Route notation — `/name` and `-> name` — is exempt and always FAILs, and it is
|
||||
the fix to recommend when the author did mean a route.
|
||||
- **`INFO boundary-target resolution DID NOT RUN` is not a pass.** The script prints it, and exits
|
||||
0, when no universe could be determined for that path — the usual cause being a skill copy
|
||||
audited outside its package. Report it as an INFO naming the unchecked targets and re-run against
|
||||
the real directory; filing it as clean signs off targets nothing verified.
|
||||
|
||||
## Script-specific failures
|
||||
|
||||
- **`validate-provenance.sh` printed nothing.** That is a pass, not a skip. It also exits 0
|
||||
silently when the skill has no `source_keys` and no `references/sources.md` — nothing to
|
||||
validate is not a finding.
|
||||
- **`vale` reports `0 files`.** Treat the pass as NOT RUN, not as clean, and fall back to full
|
||||
Step 3 judgment for the dimensions it would have covered. The bundled `Kyberforge` style is
|
||||
scoped by glob in `assets/vale/.vale.ini`; a file outside those globs is silently not linted.
|
||||
- **`E100 Runtime error ... does not exist` (exit 2) from `vale-wrap.sh`.** An explicit relative
|
||||
`--config` was passed. Pass none: the wrapper locates its own `assets/vale/.vale.ini` from its
|
||||
own path, so a resolved script path plus an unresolved config path produces exactly this. Do not
|
||||
read this exit code as vale being unavailable — that misreading sends the audit down the
|
||||
fallback path while vale was installed and working the whole time.
|
||||
- **The `vale` binary is genuinely absent** (`command not found`). Report one INFO naming it, then
|
||||
fall back to full Step 3 judgment for the description, body-discipline and patterns dimensions —
|
||||
the prefilter's whole coverage. Judge those by rubric rather than dropping them.
|
||||
- **A path argument that does not exist is a hard error** in `vale-wrap.sh`, deliberately: bare
|
||||
`vale` would fall back to reading stdin and print a clean-looking `0 errors ... in stdin`, which
|
||||
the `0 files` guard above does not catch.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -8,7 +8,14 @@ setup() {
|
||||
SCRIPT="$(cd "$BATS_TEST_DIRNAME/../scripts" && pwd)/validate.sh"
|
||||
TMPDIR="$(mktemp -d)"
|
||||
|
||||
# Helper: create a minimal valid skill directory
|
||||
# Helper: create a minimal valid skill directory.
|
||||
#
|
||||
# The description carries a boundary clause deliberately. ADR-0020's
|
||||
# missing-boundary-clause SUGGESTION fires on any description without one, so
|
||||
# a fixture that omits it is never "otherwise clean" — every test asserting
|
||||
# SUGGESTION-freedom would be asserting the boundary check's absence instead
|
||||
# of the thing it names. "anything else" is not hyphenated, so the clause adds
|
||||
# a boundary marker without adding a routing target to resolve.
|
||||
make_valid_skill() {
|
||||
local dir="$1"
|
||||
local name
|
||||
@@ -17,7 +24,7 @@ setup() {
|
||||
cat > "$dir/SKILL.md" <<EOF
|
||||
---
|
||||
name: $name
|
||||
description: A valid skill description that is well within the limit.
|
||||
description: A valid skill description that is well within the limit. Do not use for anything else.
|
||||
---
|
||||
|
||||
## Step 1
|
||||
@@ -26,6 +33,20 @@ Do the thing.
|
||||
EOF
|
||||
}
|
||||
|
||||
# Helper: a description of EXACTLY <n> characters that carries a boundary
|
||||
# clause and names no routing target. The tests below measure the description
|
||||
# LENGTH, so the clause has to be paid for out of the same budget rather than
|
||||
# appended to it — hence the padding arithmetic instead of a fixed suffix.
|
||||
desc_of_length() {
|
||||
python3 - "$1" <<'PY'
|
||||
import sys
|
||||
n = int(sys.argv[1])
|
||||
prefix = 'Use when doing the thing. Do not use for anything else. '
|
||||
assert n >= len(prefix), 'requested description shorter than the boundary clause'
|
||||
print(prefix + 'x' * (n - len(prefix)))
|
||||
PY
|
||||
}
|
||||
|
||||
# Helper: create a skill directory with an exact description length and an
|
||||
# exact body word count. <desc> is used verbatim; <body_words> "word"
|
||||
# tokens follow the frontmatter. Used by the ADR-0020 boundary tests.
|
||||
@@ -300,7 +321,7 @@ EOF
|
||||
|
||||
@test "ADR-0020: description of exactly 250 chars raises no suggestion" {
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_sized_skill "$skill" "$(python3 -c "print('x' * 250)")" 10
|
||||
make_sized_skill "$skill" "$(desc_of_length 250)" 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
refute_output --partial "SUGGESTION"
|
||||
@@ -308,7 +329,7 @@ EOF
|
||||
|
||||
@test "ADR-0020: description of 251 chars raises a SUGGESTION and still exits 0" {
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_sized_skill "$skill" "$(python3 -c "print('x' * 251)")" 10
|
||||
make_sized_skill "$skill" "$(desc_of_length 251)" 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
assert_output --partial "SUGGESTION"
|
||||
@@ -318,7 +339,7 @@ EOF
|
||||
|
||||
@test "ADR-0020: description of exactly 400 chars is a SUGGESTION, not a FAIL" {
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_sized_skill "$skill" "$(python3 -c "print('x' * 400)")" 10
|
||||
make_sized_skill "$skill" "$(desc_of_length 400)" 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
assert_output --partial "SUGGESTION"
|
||||
@@ -326,7 +347,7 @@ EOF
|
||||
|
||||
@test "ADR-0020: description of 401 chars FAILs and exits non-zero" {
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_sized_skill "$skill" "$(python3 -c "print('x' * 401)")" 10
|
||||
make_sized_skill "$skill" "$(desc_of_length 401)" 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_failure
|
||||
assert_output --partial "description is 401 chars"
|
||||
@@ -364,7 +385,7 @@ EOF
|
||||
|
||||
@test "ADR-0020: body of exactly 600 words raises no suggestion" {
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_sized_skill "$skill" "A short valid description." 600
|
||||
make_sized_skill "$skill" "A short valid description. Do not use for anything else." 600
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
refute_output --partial "SUGGESTION"
|
||||
@@ -372,7 +393,7 @@ EOF
|
||||
|
||||
@test "ADR-0020: body of 601 words raises a SUGGESTION and still exits 0" {
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_sized_skill "$skill" "A short valid description." 601
|
||||
make_sized_skill "$skill" "A short valid description. Do not use for anything else." 601
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
assert_output --partial "body is 601 words"
|
||||
@@ -381,7 +402,7 @@ EOF
|
||||
|
||||
@test "ADR-0020: body of exactly 900 words is a SUGGESTION, not a FAIL" {
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_sized_skill "$skill" "A short valid description." 900
|
||||
make_sized_skill "$skill" "A short valid description. Do not use for anything else." 900
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
assert_output --partial "body is 900 words"
|
||||
@@ -389,7 +410,7 @@ EOF
|
||||
|
||||
@test "ADR-0020: body of 901 words FAILs and exits non-zero" {
|
||||
local skill="$TMPDIR/my-skill"
|
||||
make_sized_skill "$skill" "A short valid description." 901
|
||||
make_sized_skill "$skill" "A short valid description. Do not use for anything else." 901
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_failure
|
||||
assert_output --partial "body is 901 words"
|
||||
@@ -425,15 +446,33 @@ EOF
|
||||
assert_output --partial "boundary target(s) resolve"
|
||||
}
|
||||
|
||||
@test "ADR-0020: a boundary target naming a non-existent skill FAILs" {
|
||||
@test "ADR-0020: a boundary target naming a non-existent skill FAILs when its sentence names one that resolves" {
|
||||
local skill
|
||||
skill="$(make_fixture_tree "$TMPDIR/tree" "my-skill")"
|
||||
make_sized_skill "$skill" "Use when doing the thing. Do not use for the other thing — use fixture-missing-skill instead." 10
|
||||
# `fixture-sibling-skill` is the corroborator: a prose-form target only earns
|
||||
# a FAIL when its own sentence proves it is a routing sentence. See the
|
||||
# shared resolver's CORROBORATION note, and the uncorroborated case below.
|
||||
make_sized_skill "$skill" "Use when doing the thing. Do not use for the other thing — use fixture-sibling-skill or fixture-missing-skill instead." 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_failure
|
||||
assert_output --partial "routes to 'fixture-missing-skill'"
|
||||
}
|
||||
|
||||
@test "ADR-0020: a LONE boundary target naming a non-existent skill is a SUGGESTION, not a FAIL" {
|
||||
local skill
|
||||
skill="$(make_fixture_tree "$TMPDIR/tree" "my-skill")"
|
||||
# Same grammar as the case above and as "run \`pre-commit\` instead" — a
|
||||
# route verb, a hyphenated name, terminal position. Nothing local separates a
|
||||
# broken route from a tool name, so the target is named on every run but does
|
||||
# not block: this gate ships with no baseline and no suppression mechanism.
|
||||
make_sized_skill "$skill" "Use when doing the thing. Do not use for the other thing — use fixture-missing-skill instead." 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
assert_output --partial "SUGGESTION"
|
||||
assert_output --partial "routes to 'fixture-missing-skill'"
|
||||
refute_output --partial "FAIL description routes to"
|
||||
}
|
||||
|
||||
@test "ADR-0020: a boundary target naming an AGENT file resolves (agents are valid routing targets)" {
|
||||
local skill
|
||||
skill="$(make_fixture_tree "$TMPDIR/tree" "my-skill")"
|
||||
@@ -452,15 +491,27 @@ EOF
|
||||
assert_output --partial "routes to 'fixture-missing-improve'"
|
||||
}
|
||||
|
||||
@test "ADR-0020: a backticked name that does not resolve FAILs" {
|
||||
@test "ADR-0020: a backticked name that does not resolve FAILs when its sentence names one that resolves" {
|
||||
local skill
|
||||
skill="$(make_fixture_tree "$TMPDIR/tree" "my-skill")"
|
||||
make_sized_skill "$skill" "Use when doing the thing. Composes \`fixture-missing-helper\` for the shared part." 10
|
||||
make_sized_skill "$skill" "Use when doing the thing. Composes \`fixture-sibling-skill\` and \`fixture-missing-helper\` for the shared part." 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_failure
|
||||
assert_output --partial "routes to 'fixture-missing-helper'"
|
||||
}
|
||||
|
||||
@test "ADR-0020: a /slash-command target is route NOTATION and FAILs on its own, uncorroborated" {
|
||||
local skill
|
||||
skill="$(make_fixture_tree "$TMPDIR/tree" "my-skill")"
|
||||
# The escape hatch from the SUGGESTION tier: `/name` and `-> name` are never
|
||||
# how prose cites a tool, so they are exempt from corroboration. An author
|
||||
# who wants a route checked unconditionally writes one of those two forms.
|
||||
make_sized_skill "$skill" "Use when doing the thing. Do not use for the other thing — use /fixture-missing-notation instead." 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_failure
|
||||
assert_output --partial "routes to 'fixture-missing-notation'"
|
||||
}
|
||||
|
||||
@test "ADR-0020: a bare hyphenated word outside a boundary sentence is not read as a routing target" {
|
||||
local skill
|
||||
skill="$(make_fixture_tree "$TMPDIR/tree" "my-skill")"
|
||||
@@ -502,9 +553,18 @@ EOF
|
||||
}
|
||||
|
||||
@test "ADR-0020: the boundary check declines rather than false-FAILs when no authoring source is found" {
|
||||
# Deliberately NOT built with make_fixture_tree: this skill sits in a bare
|
||||
# temp directory with no plugins/*/.apm/ above it and no .git, so the resolver
|
||||
# legitimately has no universe. That is a real path (a skill being drafted
|
||||
# outside any repo), and the required behaviour is to DECLINE OUT LOUD rather
|
||||
# than either false-FAIL or pass in silence — silence is what let a whole gate
|
||||
# family go missing unnoticed. So the INFO text and the named unchecked target
|
||||
# are both asserted, not just the absence of a failure.
|
||||
local skill="$TMPDIR/orphan/my-skill"
|
||||
make_sized_skill "$skill" "Use when doing the thing. Do not use for the other thing — use some-other-skill instead." 10
|
||||
run bash "$SCRIPT" "$skill"
|
||||
assert_success
|
||||
refute_output --partial "routes to"
|
||||
assert_output --partial "boundary-target resolution DID NOT RUN"
|
||||
assert_output --partial "Unchecked target(s): some-other-skill"
|
||||
}
|
||||
@@ -47,6 +47,7 @@ If the destination resolves inside an APM package, read `references/deployment-m
|
||||
| `references/create.md` | The create flow end to end — prerequisites, package-intent gate, scaffold, frontmatter, scripts, references, sources (loaded on demand) |
|
||||
| `references/improve.md` | The improve flow end to end — signal verification, root-cause grouping, announcement, edits (loaded on demand) |
|
||||
| `references/contract.md` | The ADR-0020 description and body contract, the Gotchas constraint, the two size gates, body patterns, and org-policy embedding (loaded on demand) |
|
||||
| `references/retrofit.md` | Bringing a pre-ADR-0020 skill into contract — ordered cut procedure, the mutually-exclusive-flows test, reference-file conventions, the collateral checklist, and a worked description retrofit (loaded from the improve flow when a budget is exceeded) |
|
||||
| `references/deployment-modes.md` | APM package vs standalone differences and self-containment/cache-isolation rules (loaded on demand) |
|
||||
| `references/scripts.md` | Package runners, inline dependency patterns, and full script contract (loaded on demand) |
|
||||
| `references/sources.md` | Upstream research sources and which skill files each contributed to |
|
||||
|
||||
@@ -19,10 +19,9 @@ metadata:
|
||||
|
||||
## Gotchas
|
||||
|
||||
- A skill's `name` and `description` are preloaded into every agent's context every session, invoked or not; the body loads only on invocation. The description is the scarce budget.
|
||||
- The word gates are two different measurements, not one rule with two tiers. The 2,770-word / 500-line spec backstop counts the whole file including frontmatter; Step 3's gate counts the body alone. A file can sit well inside one and fail the other, so never unify them.
|
||||
- Never spawn a subagent to audit or recheck your own work here. Run `/skill-audit` inline, in the same context as the edits. Clean-context recheck belongs to `/forge`'s outer loop, and a self-spawned subagent can have its worktree torn down by concurrent cleanup, destroying an uncommitted draft.
|
||||
- Do not create new scripts unless a signal explicitly calls for it. Writing one from scratch requires transcript analysis that is out of scope here — flag the opportunity as a suggestion instead.
|
||||
- The word gates are two measurements, not two tiers of one rule: the 2,770-word / 500-line spec backstop counts the whole file, Step 3's gate the body alone. Never unify them.
|
||||
- Never spawn a subagent to audit or recheck your own work — run `/skill-audit` inline, in the same context as the edits. Clean-context recheck belongs to `/forge`'s outer loop, and a self-spawned subagent's worktree can be torn down by concurrent cleanup, destroying an uncommitted draft.
|
||||
- Do not create new scripts unless a signal explicitly calls for it. Writing one from scratch requires out-of-scope transcript analysis — flag the opportunity as a suggestion instead.
|
||||
|
||||
## Step 1 — Dispatch
|
||||
|
||||
@@ -32,15 +31,15 @@ metadata:
|
||||
| Directory exists, at least one improvement signal present | Improve | `references/improve.md` |
|
||||
| Directory exists, no signals | Stop and ask | — |
|
||||
|
||||
Signals: grill output, `/skill-audit` findings, inline feedback, eval results, session context describing what went wrong. With none, ask: "No improvement signals found. Did you mean to create a new skill, or do you have feedback to apply?"
|
||||
Signals: grill output, `/skill-audit` findings, inline feedback, eval results, session context describing what went wrong. With none, ask whether the user meant to create a new skill or has feedback to apply.
|
||||
|
||||
Read only the reference matching the resolved flow — each is self-contained. Capture `git log --oneline -1` before touching the filesystem; Step 4 needs it.
|
||||
Read only the reference matching the resolved flow — each is self-contained. If the target sits inside a git worktree, capture `git log --oneline -1` before touching the filesystem; Step 4 needs it.
|
||||
|
||||
## Step 2 — Invocation axis
|
||||
|
||||
Decide before writing any description: model-invoked or hand-invoked?
|
||||
|
||||
- **Hand-invoked** — the user types `/name` and no agent should route to it. Set `disable-model-invocation: true` and write one plain human-facing sentence: no trigger list, no boundary clause. Worked example: `plugins/bin/.apm/skills/zoom-out/SKILL.md`. Skip Step 3's description rules.
|
||||
- **Hand-invoked** — the user types `/name` and no agent should route to it. Set `disable-model-invocation: true` and write one plain human-facing sentence: no trigger list, no boundary clause. Skip Step 3's description rules.
|
||||
- **Model-invoked** — the default.
|
||||
|
||||
## Step 3 — Contract
|
||||
@@ -51,12 +50,12 @@ Gates `/skill-audit` enforces in both flows:
|
||||
|
||||
- **Description** — a trigger clause, at most one capability clause, and a boundary clause shaped `Not <thing> -> <skill-name>` whose target resolves to a real skill or agent. 250 characters SUGGESTION, 400 FAIL, value only.
|
||||
- **Body** — decision procedure only: ordered steps, branches, gates, and which reference to load when. 600 words SUGGESTION, 900 FAIL, body only. At two or more mutually exclusive flows a dispatch table is mandatory and each flow gets its own self-contained `references/` file.
|
||||
- **Gotchas** — at most five, each contradicting a reasonable default. A Gotcha paraphrasing a step below it is a FAIL.
|
||||
- **Gotchas** — each contradicting a reasonable default. A Gotcha paraphrasing a step below it is a FAIL; over five entries is a SUGGESTION only.
|
||||
|
||||
## Step 4 — Validate and close
|
||||
|
||||
Run `/skill-audit` on the resolved skill directory. It checks name-to-directory match, description presence, leftover `FILL IN:` placeholders, both size budgets, boundary-target resolution and script hygiene — do not hand-check those first. Resolve every FAIL before reporting done.
|
||||
Run `/skill-audit` on the resolved skill directory; resolve every FAIL before reporting done. It checks name-to-directory match, placeholders, both size budgets, boundary-target resolution and script hygiene — do not hand-check those. Hand-check the one thing it misses: an empty body reports `PASS SKILL.md body word count 0 (ADR-0020 target: 600)`, so confirm at least one non-empty section exists.
|
||||
|
||||
With `metadata.version` present, bump the **minor** version on create (new skills start at `0.1.0`) and the **patch** version on improve.
|
||||
|
||||
**Commit verification.** Once the audit is clean, run `git add` and `git commit` — do not stop at staging. Re-run `git log --oneline -1` and confirm the hash changed from the one captured at Step 1. A non-empty `git diff --stat` is not proof: staged-but-uncommitted work is part of no commit and is silently lost if the tree is cleaned up first. Report done only once the hash has changed.
|
||||
**Commit verification.** Inside a git worktree: once the audit is clean, run `git add` and `git commit` — do not stop at staging. Re-run `git log --oneline -1` and confirm the hash changed from Step 1's. A non-empty `git diff --stat` is not proof: staged-but-uncommitted work is part of no commit and is silently lost if the tree is cleaned up. Report done only once the hash has changed. Outside a worktree (a skill under `~/.claude/skills/`, say) nothing is committable — report done on a clean audit, naming that as the reason.
|
||||
@@ -10,14 +10,22 @@ name: SKILL_NAME
|
||||
# Examples: my-tool, data-analyzer, pdf-processor
|
||||
|
||||
description: >
|
||||
Use when FILL IN: trigger — when should an agent activate this skill?
|
||||
Use when FILL IN: trigger.
|
||||
FILL IN: at most ONE capability clause, stated specifically
|
||||
(e.g. "parses and validates OpenAPI specs", not "helps with APIs").
|
||||
Not FILL IN: near-miss case -> FILL IN: real sibling skill name.
|
||||
Not FILL IN: near-miss case -> FILL IN: real sibling skill.
|
||||
# Required. Preloaded into EVERY session whether or not the skill is invoked.
|
||||
# Exactly three parts, in this order: trigger clause, at most one capability
|
||||
# clause, boundary clause. Drop the boundary line if no near-miss skill exists.
|
||||
# Budget: 250 characters target, 400 hard ceiling (counting this value only).
|
||||
# Trigger clause: when should an agent activate this skill? Describe the user's
|
||||
# intent, not the skill's internal mechanics.
|
||||
# Budget: 250 characters target, 400 hard ceiling (counting this value only,
|
||||
# with YAML folding resolved). This scaffold sits at 214 — keep the fill-in
|
||||
# under the target rather than growing past it.
|
||||
# Boundary clauses may be plural: write one per genuine near-miss, and none
|
||||
# where no sibling could steal activations.
|
||||
# Never let a hyphenated skill name wrap across two lines of this folded block
|
||||
# — folding turns the break into a space and the routing target stops resolving.
|
||||
# Banned here: capability lists, output-format detail, composition notes,
|
||||
# implementation detail, and restating one trigger twice in two registers.
|
||||
# The boundary target must resolve to a real skill or agent — it is checked.
|
||||
|
||||
@@ -47,11 +47,21 @@ explicitly" only where the user's natural phrasing genuinely omits the domain wo
|
||||
for `git-commits`, where the user says "commit". Adding one everywhere is what inflated this
|
||||
corpus, and it was deleted as a blanket rule.
|
||||
|
||||
**Boundary targets must resolve.** The name after the arrow is checked against real skill
|
||||
directories under `plugins/*/.apm/skills/<name>/` and real agents under
|
||||
`plugins/*/.apm/agents/<name>.agent.md`. A boundary clause naming a target that does not exist
|
||||
sends the router nowhere and fails the audit. Check the target exists before writing it — do not
|
||||
invent a plausible sibling name.
|
||||
**Boundary targets must resolve.** Both forms are checked — the arrow and the prose form ("do not
|
||||
use for X, use `y` instead") — so a typo dangles either way. Targets resolve against a universe
|
||||
built by walking up **from the SKILL.md itself**: the nearest ancestor holding
|
||||
`plugins/*/.apm/{skills,agents}` (or, failing that, the nearest ancestor holding `.git`) contributes
|
||||
every skill and agent under `<root>/plugins/*/`, plus the skill's own apm package and the packages
|
||||
that package declares in `apm.yml` under `dependencies.apm`. A sibling plugin in the same monorepo
|
||||
therefore resolves; a skill in an unrelated repo does not. A boundary clause naming a target
|
||||
outside that universe sends the router nowhere and fails the audit. Check the target exists before
|
||||
writing it — do not invent a plausible sibling name.
|
||||
|
||||
That universe is the apm marketplace and stops there. A **host built-in is not a routing target**:
|
||||
`/compact`, `/clear` and `/init` are Claude Code slash commands with no counterpart in Copilot CLI
|
||||
or Codex, and `.apm/` source compiles for all three, so routing to one is a portability defect. The
|
||||
gate is right to fail it and there is no allowlist. If a built-in genuinely needs mentioning, write
|
||||
it un-slashed — ``the `compact` built-in`` — which makes no routing claim and is not checked.
|
||||
|
||||
**Length.** 250 characters SUGGESTION, 400 characters FAIL, counting the frontmatter value only
|
||||
with YAML folding resolved. The agentskills.io 1,024-character spec limit is unchanged and sits
|
||||
@@ -61,7 +71,13 @@ as the outlier stop.
|
||||
**Hand-invoked skills are exempt.** A skill carrying `disable-model-invocation: true` is absent
|
||||
from the model-visible listing and is reached only by the user typing `/name`. It takes one plain
|
||||
human-facing sentence — no trigger clause, no boundary clause, no indirect triggers. Worked
|
||||
example: `plugins/bin/.apm/skills/zoom-out/SKILL.md`.
|
||||
example — the whole description of the `zoom-out` skill, which carries `disable-model-invocation`:
|
||||
|
||||
````markdown
|
||||
Tell the agent to zoom out and give broader context or a higher-level perspective. Use when
|
||||
you're unfamiliar with a section of code or need to understand how it fits into the bigger
|
||||
picture.
|
||||
````
|
||||
|
||||
## Body
|
||||
|
||||
@@ -90,6 +106,12 @@ blocks, rationale prose, and any content only one branch reaches. Each reference
|
||||
self-contained for its concern, and every one is wired from the body with the literal conditional
|
||||
form:
|
||||
|
||||
**The one exception, stated once so it is not re-litigated:** an output schema stays in the body
|
||||
only when it applies to *every* flow and is short — roughly 50 words or less, which is the "Output
|
||||
format template" pattern below. An output schema that is longer than that, or that only one flow
|
||||
produces, moves to `references/` like any other schema. No third option exists, and the two rules
|
||||
do not disagree.
|
||||
|
||||
````markdown
|
||||
If <condition>, read `references/<file>.md`.
|
||||
````
|
||||
@@ -98,8 +120,9 @@ A generic pointer ("see references/ for details") is a Vale error — the agent
|
||||
|
||||
**Dispatch is mandatory at two or more mutually exclusive flows.** The body carries the dispatch
|
||||
table and the gates common to every branch; each flow gets its own self-contained `references/`
|
||||
file. Exemplar: `plugins/kyberforge/.apm/skills/apm-workflow/SKILL.md` — a 554-word body
|
||||
dispatching to 3,006 words of references.
|
||||
file. Exemplar: the `apm-workflow` skill — a **421-word body** dispatching to 3,006 words of
|
||||
references. Calibrate against 421: that file's whole-file count is 554 words, and aiming at that
|
||||
number instead overshoots the body budget by ~30%.
|
||||
|
||||
**Length.** 600 words SUGGESTION, 900 words FAIL, counting the **body only** — everything after
|
||||
the frontmatter's closing `---`.
|
||||
@@ -108,7 +131,7 @@ the frontmatter's closing `---`.
|
||||
|
||||
- Each entry must state a fact that **contradicts a reasonable default** — something the agent
|
||||
gets wrong by acting sensibly. "Never commit secrets" is not one; the agent already knows.
|
||||
- Maximum five entries.
|
||||
- More than five entries is a SUGGESTION — five is the guideline, not a ceiling.
|
||||
- A Gotcha that paraphrases a step in the body below it is a **FAIL**. If the rule is already a
|
||||
step, it is not a gotcha.
|
||||
- A Gotchas section exceeding 25% of the body is a SUGGESTION.
|
||||
@@ -164,7 +187,9 @@ Do not modify flags.
|
||||
| <condition> | <flow> | `references/<file>.md` |
|
||||
````
|
||||
|
||||
**Output format template** (when the skill produces structured output):
|
||||
**Output format template** (when the skill produces structured output on *every* flow, and the
|
||||
schema is roughly 50 words or less — see the exception under Body above; anything longer or
|
||||
flow-specific belongs in `references/`):
|
||||
|
||||
````markdown
|
||||
Output format:
|
||||
@@ -173,7 +198,8 @@ Output format:
|
||||
```
|
||||
````
|
||||
|
||||
For longer templates, place them in `assets/<name>.md` and reference conditionally.
|
||||
For longer templates, place them in `references/<topic>.md` or `assets/<name>.md` and reference
|
||||
conditionally.
|
||||
|
||||
## Embedding org-specific policy
|
||||
|
||||
|
||||
@@ -136,10 +136,16 @@ If no scripts are needed, delete `scripts/README.md` and the `scripts/` director
|
||||
|
||||
## Step 5 — Add references, assets, and tests (if needed)
|
||||
|
||||
**`references/`** — additional documentation loaded on demand. One topic per file. Reference
|
||||
conditionally from SKILL.md with the literal form ``If <condition>, read `references/<file>.md` ``.
|
||||
Keep reference chains one level deep — a reference file that references another reference file is
|
||||
rarely loaded correctly.
|
||||
**`references/`** — additional documentation loaded on demand. One topic per file, named in
|
||||
kebab-case after the topic. Reference conditionally from SKILL.md with the literal form
|
||||
``If <condition>, read `references/<file>.md` ``.
|
||||
|
||||
**Two hops from `SKILL.md`, never three.** A flow file may route on to a shared contract or
|
||||
sub-topic file — that is the shipped pattern here (`SKILL.md` → `references/create.md` → this
|
||||
file's own pointers to `contract.md`, `scripts.md` and `deployment-modes.md`). What does not work
|
||||
is a third hop: a file reachable only through two intermediates is rarely loaded at the moment it
|
||||
is needed. Every hop past the first also needs the same literal conditional form, so the agent
|
||||
knows when to take it.
|
||||
|
||||
**`assets/`** — static resources: templates, schemas, lookup tables. Reference by relative path
|
||||
from SKILL.md.
|
||||
|
||||
@@ -76,7 +76,19 @@ contract first — the gates are hot and carry no baseline file, so a one-line f
|
||||
non-compliant skill cannot be committed until the description and body meet
|
||||
`references/contract.md`. Treat that retrofit as part of the same change, not a follow-up.
|
||||
|
||||
If the skill's description exceeds 250 characters, or its body-only word count exceeds 600, read
|
||||
`references/retrofit.md` before editing. It carries the ordered cut procedure, the
|
||||
mutually-exclusive-flows test, the reference-file conventions this flow needs, the collateral
|
||||
checklist for `README.md` and `references/sources.md`, and a worked description retrofit. Do not
|
||||
improvise the cuts — four dry runs invented six to ten different answers to the same questions.
|
||||
|
||||
If a signal points to a script or reference file, edit that file directly rather than adding a
|
||||
workaround in SKILL.md.
|
||||
|
||||
**Check for regressions before handing back.** `SKILL.md` Step 4 tells you to resolve every FAIL,
|
||||
which says nothing about a check that passed *before* these edits and no longer does. Compare the
|
||||
closing audit against the skill's pre-edit state — a PASS that has become a SUGGESTION, or a
|
||||
SUGGESTION that has become a FAIL, is damage this flow caused and is in scope for it. Only the
|
||||
improve flow can make that comparison; the create flow has no prior state to compare against.
|
||||
|
||||
Then return to `SKILL.md` Step 4.
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
source_keys:
|
||||
- agentskills-best-practices
|
||||
- agentskills-optimizing-descriptions
|
||||
---
|
||||
|
||||
# Retrofitting a skill to the ADR-0020 contract
|
||||
|
||||
Read this when `references/improve.md` Step 4 sends you here: the skill you are editing is over
|
||||
the description or body budget and has to come into contract before any other change can be
|
||||
committed. The gates are hot and carry no baseline file, so a one-line fix to a non-compliant
|
||||
skill is blocked until this is done.
|
||||
|
||||
Measure first. Do not guess which gate fired: run `/skill-audit` on the directory and read its
|
||||
`### Structure` dimension, which reports the description characters and the **body-only** word
|
||||
count separately from the whole-file spec backstop. Retrofit against the number that actually
|
||||
fired — a skill can sit a thousand words inside the whole-file backstop while failing the body
|
||||
budget.
|
||||
|
||||
**Validate in place.** Audit the skill's real directory inside its package. Never audit a copy in a
|
||||
scratch directory, and never move a skill out to work on it: the boundary-target universe is built
|
||||
by walking up *from the file being checked*, so a copy with no authoring root above it resolves
|
||||
against nothing and the check declines rather than running —
|
||||
|
||||
```text
|
||||
INFO boundary-target resolution DID NOT RUN — no skill universe could be determined for
|
||||
this path ... Unchecked target(s): totally-fake-target
|
||||
```
|
||||
|
||||
The run still exits 0, so that line reads as a pass and is not one. Treat `DID NOT RUN` as **not
|
||||
checked**, always. A retrofit signed off on a scratch copy carries an unverified boundary target
|
||||
into the corpus, which is precisely the failure this gate exists to catch.
|
||||
|
||||
## Cut in this order
|
||||
|
||||
Work the list top down and stop as soon as the gate clears. The order is by ratio of tokens
|
||||
removed to behaviour lost — inverting it is how a retrofit ends up deleting the one instruction
|
||||
the skill existed to carry.
|
||||
|
||||
1. **Gotchas that paraphrase a step in the body below.** Zero information, and already a FAIL on
|
||||
its own. Delete the Gotcha, keep the step.
|
||||
2. **Spec restatements** — text that repeats a published specification, a tool's `--help`, or a
|
||||
ceiling the validator already enforces. The agent gets this right without it. Delete, or move
|
||||
the table to `references/` if a flow genuinely needs to look it up.
|
||||
3. **Capability enumeration** — in a description, the feature list after the trigger clause; in a
|
||||
body, the paragraph that recites what the skill can do. One capability clause survives in the
|
||||
description; the rest belongs in `README.md`.
|
||||
4. **Per-flow prose** — anything only one branch of the procedure ever reaches. This is the
|
||||
largest single win in most bodies, and it is a *move*, not a delete: each flow gets its own
|
||||
self-contained `references/` file, wired from a dispatch table.
|
||||
|
||||
If the body is still over after all four, the skill is doing two jobs. Split it, and say so
|
||||
rather than compressing prose until it stops being readable.
|
||||
|
||||
## What "mutually exclusive flows" means
|
||||
|
||||
Two or more flows that a single invocation cannot both take. The three-way test, copied verbatim
|
||||
from the body-discipline rubric `/skill-audit` judges against — nothing to load, it is quoted in
|
||||
full here:
|
||||
|
||||
> separate subcommands, separate input types, separate lifecycle stages
|
||||
|
||||
Any one of the three is enough. Two flows that differ only in a parameter value are one flow.
|
||||
At two or more mutually exclusive flows a dispatch table is **mandatory** regardless of word
|
||||
count, because every invocation otherwise pays for every branch it did not take.
|
||||
|
||||
## Reference-file conventions
|
||||
|
||||
The create flow owns these rules, and this flow is forbidden from reading `references/create.md`,
|
||||
so what a retrofit needs is restated here:
|
||||
|
||||
- **One topic per file.** A file mixing two concerns gets loaded for one of them and spends the
|
||||
caller's context on the other.
|
||||
- **Kebab-case filenames**, named after the topic rather than the flow that reads it —
|
||||
`body-discipline.md`, not `step-3.md`.
|
||||
- **Wire every file with the literal conditional form** ``If <condition>, read
|
||||
`references/<file>.md` ``. A generic pointer ("see `references/` for details") is a Vale error.
|
||||
- **Two hops from `SKILL.md`, never three.** A flow file may route on to a shared contract file;
|
||||
a file reachable only through two intermediates is rarely loaded when it is needed.
|
||||
- **`source_keys` frontmatter.** If the content you are moving drew on a research source, the new
|
||||
file needs top-level `source_keys:` frontmatter listing those slugs, and every slug must already
|
||||
exist as an `## <slug>` heading in `references/sources.md`. Moving sourced content out of
|
||||
`SKILL.md` without carrying its slugs across breaks the provenance chain, and `/skill-audit`
|
||||
reports the new file as an INFO with no `source_keys`.
|
||||
|
||||
## Collateral is mandatory, not optional
|
||||
|
||||
Moving content out of a `SKILL.md` leaves three files describing a structure that no longer
|
||||
exists. `/skill-audit`'s provenance check exits clean on all three of these, so nothing catches
|
||||
them for you. After every retrofit that adds, removes or renames a file:
|
||||
|
||||
- [ ] **`README.md` file table** — a row for every new `references/` file, and no row left for a
|
||||
file that is gone. Say what triggers the load, not just what the file contains.
|
||||
- [ ] **`references/README.md`**, where the skill has one — same update, same reason.
|
||||
- [ ] **`references/sources.md` → `Contributing files`** — add the new file to every slug whose
|
||||
content moved into it, and remove any file the retrofit deleted. This is the one that gets
|
||||
missed: `sources.md` keeps citing sections of `SKILL.md` that no longer exist, the
|
||||
provenance check still exits 0, and the stale claim survives review.
|
||||
- [ ] Re-run `/skill-audit` and confirm its `### Provenance` dimension does not report the new
|
||||
file as missing `source_keys`.
|
||||
|
||||
## Worked example — a description retrofit
|
||||
|
||||
`gitea-issues` before, 827 characters, the single most common shape in the corpus:
|
||||
|
||||
```text
|
||||
Use when reading or writing Gitea issues: listing repo issues, getting a single issue's details/
|
||||
comments/labels, creating an issue, updating its state, adding or editing comments, applying
|
||||
labels via issue_write, or searching issues/PRs across repositories. Triggers on "create an
|
||||
issue", "what issues are open", "get issue #N", "close issue #N", "comment on issue #N", "search
|
||||
issues for X" — even when the user doesn't say "Gitea" explicitly. Composes gitea-labels-
|
||||
milestones for all label inference/resolution and milestone lookup — do not use this skill to
|
||||
manage label or milestone definitions themselves (create/edit/delete a label, create/close a
|
||||
milestone), that's gitea-labels-milestones directly. Do not use for pull requests (use gitea-prs)
|
||||
or for local git branch/commit work (use gitea-branches or git-branches).
|
||||
```
|
||||
|
||||
After, 240 characters:
|
||||
|
||||
```text
|
||||
Use when reading or writing Gitea issues — list, read, create, comment on, label, close, or
|
||||
search — even when the user does not say "Gitea". Not pull requests -> `gitea-prs`. Not label or
|
||||
milestone definitions -> `gitea-labels-milestones`.
|
||||
```
|
||||
|
||||
What came out, and why:
|
||||
|
||||
| Removed | Why |
|
||||
|---|---|
|
||||
| The second trigger register — `Triggers on "create an issue", "what issues are open", …` | The same triggers restated as quoted user phrasings. Two registers of one trigger list is a FAIL, not a suggestion. |
|
||||
| `applying labels via issue_write` | Implementation detail. The router does not choose a skill by which MCP call it makes. |
|
||||
| `Composes gitea-labels-milestones for all label inference/resolution and milestone lookup` | A composition note. It changes no routing decision and belongs in `README.md`. |
|
||||
| The parenthetical `(create/edit/delete a label, create/close a milestone)` | Capability enumeration inside a boundary clause. The boundary needs the target, not its feature list. |
|
||||
| The `gitea-branches` / `git-branches` boundary | Dropped entirely. Neither was ever going to win an issue request, so the clause defended against nothing — an invented boundary costs characters and buys no routing accuracy. |
|
||||
| `Do not use for pull requests (use gitea-prs)` prose form | Kept, but rewritten as `Not pull requests -> \`gitea-prs\`.` The rewrite buys characters and one uniform shape for the router — not safety. Both forms are parsed **and** target-checked, so a typo in the prose form dangles exactly as an arrow typo does. |
|
||||
|
||||
What stayed: one trigger clause, one capability clause, the indirect trigger (genuinely warranted
|
||||
here — people say "create an issue", not "create a Gitea issue"), and the boundary clauses.
|
||||
|
||||
## Two rules the gates enforce but the prose does not spell out
|
||||
|
||||
**Boundary clauses may be plural.** Write one per genuine near-miss — the example above carries
|
||||
two, because two different skills could each steal activations. "A boundary clause" in the
|
||||
contract means *at least one*, not *exactly one*. What is banned is a boundary clause invented for
|
||||
a skill that was never going to compete, not a second real one.
|
||||
|
||||
**Never let a hyphenated routing target wrap across lines in a folded `>` scalar.** YAML folding
|
||||
replaces the newline with a space, so `gitea-labels-` at the end of one line and `milestones` at
|
||||
the start of the next fold into `gitea-labels- milestones`. `validate.sh` then reads the target as
|
||||
`gitea-labels`, finds no such skill, and reports a dangling boundary target — the live finding on
|
||||
`gitea-issues` today. Reflow the line so the whole name sits on one of them. The same applies to
|
||||
any backticked skill or agent name in a description.
|
||||
@@ -34,7 +34,7 @@ source_keys:
|
||||
- **URL:** https://agentskills.io/skill-creation/best-practices.md
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
|
||||
- **Description:** Best practices for skill creators — starting from real expertise, spending context wisely, calibrating control, instruction patterns (gotchas, templates, checklists, validation loops)
|
||||
- **Contributing files:** SKILL.md, references/create.md, references/improve.md, references/contract.md
|
||||
- **Contributing files:** SKILL.md, references/create.md, references/improve.md, references/contract.md, references/retrofit.md
|
||||
- **Status:** `extracted`
|
||||
|
||||
## agentskills-optimizing-descriptions
|
||||
@@ -42,7 +42,7 @@ source_keys:
|
||||
- **URL:** https://agentskills.io/skill-creation/optimizing-descriptions.md
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
|
||||
- **Description:** How to systematically test and improve skill descriptions for triggering accuracy — eval queries, trigger rate testing, train/validation splits, optimization loop
|
||||
- **Contributing files:** SKILL.md, references/improve.md, references/contract.md
|
||||
- **Contributing files:** SKILL.md, references/improve.md, references/contract.md, references/retrofit.md
|
||||
- **Status:** `extracted`
|
||||
|
||||
## agentskills-evaluating-skills
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kyberforge",
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.0",
|
||||
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kyberforge",
|
||||
"version": "1.5.0",
|
||||
"version": "1.6.0",
|
||||
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
||||
"author": {
|
||||
"name": "Defame1297",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: kyberforge
|
||||
version: 1.5.0
|
||||
version: 1.6.0
|
||||
description: Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.
|
||||
author:
|
||||
name: Defame1297
|
||||
|
||||
@@ -76,6 +76,9 @@ Include what the fresh context lacks:
|
||||
- A direct role instruction opening the prompt: `You are a [role]. When invoked, [action].`
|
||||
- One bounded job, stated so the agent knows what it must refuse.
|
||||
- The dispatch, gates, inputs and outputs listed above.
|
||||
- **Error handling** — what the agent does on malformed, missing or contradictory input: stop and
|
||||
report, or degrade to a named fallback. Absent it, the agent invents a recovery, and a
|
||||
subagent's invented recovery is invisible to its caller until the output is wrong.
|
||||
- Non-obvious environment facts and project-specific conventions it cannot infer.
|
||||
- One default per decision point with one escape hatch.
|
||||
|
||||
@@ -111,6 +114,8 @@ Flag as FAIL if:
|
||||
Flag as SUGGESTION if:
|
||||
|
||||
- The body does not open with a direct role instruction
|
||||
- The body specifies no error handling — nothing tells the agent what to do with malformed,
|
||||
missing or contradictory input
|
||||
- The job the agent describes is unbounded, or bounded only implicitly
|
||||
- A rationale is missing from a rule the agent is expected to enforce — present but unexplained
|
||||
- Comments are useful but verbose enough to bury the field they annotate
|
||||
@@ -111,9 +111,12 @@ Flag as FAIL if:
|
||||
available). `Kyberforge.VagueWording` catches the known filler; imprecision outside that list is
|
||||
judgment.
|
||||
- **A boundary clause naming a target that does not resolve** to a real skill directory or agent
|
||||
file in the authoring source. No script checks this for an agent file — `validate.sh` resolves
|
||||
boundary targets for skills only, so resolve the name yourself against `plugins/*/.apm/skills/`
|
||||
and `plugins/*/.apm/agents/`.
|
||||
file in the authoring source. `validate.sh` resolves this for agent files at both scopes and
|
||||
reports each unresolved target itself — take its verdict rather than re-resolving the name by
|
||||
hand, because a hand-walk over a different universe can contradict it. What is left to you is
|
||||
semantic and the script cannot reach it: whether a target that *does* resolve is the right
|
||||
sibling to exclude, and whether a clause naming no target at all ("examine the files manually")
|
||||
should have named one.
|
||||
- **`Use proactively` in a Copilot or vendor-neutral description.**
|
||||
`KyberforgeCopilot.ProactivePhrase` catches it. The phrase steers the Claude Code runtime and
|
||||
does nothing anywhere else, so in a `.agent.md` it is preloaded text that buys no behaviour.
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -53,6 +53,8 @@ Gates `agent-audit` enforces at every scope:
|
||||
- **Body** — no word gate, and a delegation check in its place: name the skill to invoke rather than restating what it does.
|
||||
- **Invocation** — decide whether the agent is model-delegated or reached only by name. Only Copilot's cloud/IDE format expresses that in frontmatter (`disable-model-invocation`, `user-invocable`).
|
||||
|
||||
At every scope, five tools reach no subagent whatever `tools` says — `AskUserQuestion`, `EnterPlanMode`, `ExitPlanMode`, `ScheduleWakeup`, `WaitForMcpServers`. Never write a body that has the agent ask the user a question or enter plan mode; it describes a turn the runtime cannot give it.
|
||||
|
||||
## Step 4 — Validate and close
|
||||
|
||||
Invoke `agent-audit` on each file written and resolve every FAIL before reporting done. It checks the field allowlist, name-to-stem match, leftover placeholders and template comments, the description budget and the Copilot body limit — do not hand-check those.
|
||||
|
||||
@@ -34,10 +34,13 @@ description: FILL IN: Use when <trigger>. <One capability clause.> Not <thing> -
|
||||
boundary clause naming a real sibling skill or agent.
|
||||
250 characters is the target, 400 the hard ceiling (ADR-0020).
|
||||
Do not open with an action verb ("Reviews...", "Analyzes...") — that rule was
|
||||
deleted. Add "Use proactively" only if the runtime should delegate here without
|
||||
the user naming this agent.
|
||||
deleted.
|
||||
Never write "Use proactively" here. It steers the Claude Code runtime and does
|
||||
nothing anywhere else, and this file compiles to a Copilot `.agent.md` too, where
|
||||
agent-audit's KyberforgeCopilot.ProactivePhrase rule grades it a hard FAIL.
|
||||
The phrase is CC-only; at this scope, a precise trigger clause does that job.
|
||||
Example: "Use when a diff needs checking for injected credentials before it
|
||||
merges. Not general code review -> code-reviewer." -->
|
||||
merges. Not prose or style linting -> `lint-runner`." -->
|
||||
|
||||
<!-- model: sonnet
|
||||
Optional. Aliases: sonnet, opus, haiku, fable. Or full model ID.
|
||||
@@ -80,3 +83,10 @@ FILL IN: Steps the agent takes. Be specific about ordering if it matters.
|
||||
## Output
|
||||
|
||||
FILL IN: What does the agent produce? Format, location, structure.
|
||||
|
||||
## Errors
|
||||
|
||||
FILL IN: What does the agent do on malformed, missing or contradictory input?
|
||||
State whether it stops and reports, or degrades to a named fallback — and what it
|
||||
tells the caller either way. An agent with no error handling invents a recovery,
|
||||
and an invented recovery is invisible until the output is wrong.
|
||||
@@ -14,19 +14,24 @@ description: FILL IN: Use when <trigger>. <One capability clause.> Not <thing> -
|
||||
boundary clause naming a real sibling skill or agent.
|
||||
250 characters is the target, 400 the hard ceiling (ADR-0020).
|
||||
Do not open with an action verb ("Reviews...", "Analyzes...") — that rule was
|
||||
deleted. Add "Use proactively" only if the runtime should delegate here without
|
||||
the user naming this agent.
|
||||
deleted.
|
||||
"Use proactively" is valid HERE and only here: it steers the Claude Code runtime
|
||||
to offer this agent unprompted. Add it only if that is what you want. If you add
|
||||
it, leave it OUT of the Copilot half of the pair — the phrase does nothing there
|
||||
and agent-audit's KyberforgeCopilot.ProactivePhrase grades it a hard FAIL. The
|
||||
pair must describe the same job; it does not have to be byte-identical.
|
||||
Example: "Use when a diff needs checking for injected credentials before it
|
||||
merges. Not general code review -> code-reviewer." -->
|
||||
merges. Not prose or style linting -> `lint-runner`." -->
|
||||
|
||||
<!-- tools: Read, Bash, Grep
|
||||
Optional. Allowlist of tool names: a comma-separated string or a YAML list.
|
||||
Omit to inherit all tools from parent.
|
||||
Restrict it to what the agent actually needs. Omit only when it needs them
|
||||
all — omitting inherits every tool from the parent.
|
||||
Use Agent(type1,type2) to restrict which subagent types this agent can spawn.
|
||||
Omit Agent entirely to prevent this agent from spawning subagents.
|
||||
Never available to subagents regardless of tools field:
|
||||
AskUserQuestion, EnterPlanMode, ExitPlanMode, ScheduleWakeup, WaitForMcpServers
|
||||
Exception: ExitPlanMode IS available when parent session runs in permissionMode: plan -->
|
||||
Listing any of them is a finding: agent-audit enforces the flat rule. -->
|
||||
|
||||
<!-- model: sonnet
|
||||
Optional. Aliases: sonnet, opus, haiku, fable. Or full model ID.
|
||||
@@ -99,3 +104,10 @@ FILL IN: Steps the agent takes. Be specific about ordering if it matters.
|
||||
## Output
|
||||
|
||||
FILL IN: What does the agent produce? Format, location, structure.
|
||||
|
||||
## Errors
|
||||
|
||||
FILL IN: What does the agent do on malformed, missing or contradictory input?
|
||||
State whether it stops and reports, or degrades to a named fallback — and what it
|
||||
tells the caller either way. An agent with no error handling invents a recovery,
|
||||
and an invented recovery is invisible until the output is wrong.
|
||||
@@ -19,9 +19,13 @@ description: FILL IN: Use when <trigger>. <One capability clause.> Not <thing> -
|
||||
boundary clause naming a real sibling skill or agent.
|
||||
250 characters is the target, 400 the hard ceiling (ADR-0020).
|
||||
Do not open with an action verb ("Reviews...", "Analyzes...") — that rule was deleted.
|
||||
Keep it identical in wording to the Claude Code half of the pair.
|
||||
Never write "Use proactively" here. It steers the Claude Code runtime and does nothing
|
||||
in Copilot, and agent-audit's KyberforgeCopilot.ProactivePhrase grades it a hard FAIL.
|
||||
Otherwise keep the wording matched to the Claude Code half of the pair: agent-audit
|
||||
checks that both halves describe the same job, not that they are byte-identical, so
|
||||
dropping the CC-only phrase here is not a pair-consistency finding.
|
||||
Example: "Use when a diff needs checking for injected credentials before it
|
||||
merges. Not general code review -> code-reviewer." -->
|
||||
merges. Not prose or style linting -> `lint-runner`." -->
|
||||
|
||||
<!-- tools: ["read", "search", "edit"]
|
||||
Optional. Array of tool names. Omit = all available tools. [] = no tools.
|
||||
@@ -68,3 +72,10 @@ FILL IN: Steps the agent takes. Be specific about ordering if it matters.
|
||||
## Output
|
||||
|
||||
FILL IN: What does the agent produce? Format, location, structure.
|
||||
|
||||
## Errors
|
||||
|
||||
FILL IN: What does the agent do on malformed, missing or contradictory input?
|
||||
State whether it stops and reports, or degrades to a named fallback — and what it
|
||||
tells the caller either way. An agent with no error handling invents a recovery,
|
||||
and an invented recovery is invisible until the output is wrong.
|
||||
@@ -44,15 +44,37 @@ Banned from a description; move it to the body or to `README.md`:
|
||||
and ADR-0020 deleted it: the opener is `Use when`, matching every skill in this corpus, so one
|
||||
router reads one shape.
|
||||
|
||||
**"Use proactively" is conditional.** Add it only where the runtime should delegate without the
|
||||
user naming the agent — an agent invoked by name does not need it, and it costs activations
|
||||
elsewhere when added by reflex. The same conditional governs indirect triggers ("even if the user
|
||||
doesn't say X"): add one only where the user's natural phrasing genuinely omits the domain word.
|
||||
**"Use proactively" is Claude Code-only, and conditional even there.** The phrase steers the
|
||||
Claude Code runtime to offer an agent unprompted and does nothing anywhere else, so where it may
|
||||
appear depends on the file:
|
||||
|
||||
**Boundary targets must resolve.** The name after the arrow is checked against real skills under
|
||||
`plugins/*/.apm/skills/<name>/` and real agents under `plugins/*/.apm/agents/<name>.agent.md`. A
|
||||
target that does not exist sends the router nowhere. Verify it before writing it — do not invent a
|
||||
plausible sibling.
|
||||
| File | Rule |
|
||||
|---|---|
|
||||
| Claude Code `.md` (project/user scope) | Allowed. Add it only where the runtime should delegate without the user naming the agent — an agent invoked by name does not need it, and it costs activations elsewhere when added by reflex. |
|
||||
| Copilot `.agent.md` (project/user scope) | **Never.** Inert there, and `KyberforgeCopilot.ProactivePhrase` grades it a hard FAIL. |
|
||||
| Vendor-neutral `.apm/agents/<name>.agent.md` (plugin/APM scope) | **Never.** Same Vale rule, same hard FAIL — the file matches the `**/*.agent.md` glob, and it compiles to a real Copilot agent downstream. |
|
||||
|
||||
A pair whose Claude Code half carries the phrase and whose Copilot half omits it is correct, not
|
||||
inconsistent: `agent-audit` checks that both halves describe the same job, not that they match
|
||||
word for word.
|
||||
|
||||
Indirect triggers ("even if the user doesn't say X") take a similar conditional at every scope:
|
||||
add one only where the user's natural phrasing genuinely omits the domain word.
|
||||
|
||||
**Boundary targets must resolve.** Both forms are checked — the arrow and the prose form ("do not
|
||||
use for X, use `y` instead") — so a typo dangles either way. Targets resolve against a universe
|
||||
built by walking up **from the agent file itself**: the nearest ancestor holding
|
||||
`plugins/*/.apm/{skills,agents}` (or, failing that, the nearest ancestor holding `.git`) contributes
|
||||
every skill and agent under `<root>/plugins/*/`, plus the agent's own apm package and the packages
|
||||
that package declares in `apm.yml` under `dependencies.apm`. A sibling plugin in the same monorepo
|
||||
therefore resolves; a skill in an unrelated repo does not. A target outside that universe sends the
|
||||
router nowhere. Verify it before writing it — do not invent a plausible sibling.
|
||||
|
||||
That universe is the apm marketplace and stops there. A **host built-in is not a routing target**:
|
||||
`/compact`, `/clear` and `/init` are Claude Code slash commands with no counterpart in Copilot CLI
|
||||
or Codex, and `.apm/` source compiles for all three, so routing to one is a portability defect. The
|
||||
gate is right to fail it and there is no allowlist. If a built-in genuinely needs mentioning, write
|
||||
it un-slashed — ``the `compact` built-in`` — which makes no routing claim and is not checked.
|
||||
|
||||
**Length.** 250 characters SUGGESTION, 400 characters FAIL, counting the frontmatter value only
|
||||
with YAML folding resolved. Treat 250 as the target: the SUGGESTION tier is what moves the corpus
|
||||
@@ -73,8 +95,18 @@ You are a <role>. When invoked, <primary action>.
|
||||
|
||||
## Output
|
||||
<what it produces: format, location, structure>
|
||||
|
||||
## Errors
|
||||
<what to do on malformed, missing or contradictory input: report and stop, or
|
||||
which fallback to take — and what to say to the caller either way>
|
||||
````
|
||||
|
||||
Four required elements: **inputs expected, process steps, output format, error handling.** The
|
||||
last is the one that gets dropped, and dropping it is not neutral: an agent given a malformed
|
||||
input and no instruction invents a recovery, and a subagent's invented recovery is invisible to
|
||||
the caller until the output is wrong. Say explicitly whether the agent stops and reports, or
|
||||
degrades to a named fallback.
|
||||
|
||||
One job per agent. An agent covering two jobs gets delegated to for the wrong one.
|
||||
|
||||
**Delegation discipline replaces the word gate.** A plugin/APM agent is a single file with no
|
||||
|
||||
@@ -57,6 +57,11 @@ procedure a skill it can invoke already owns is an `agent-audit` FAIL. When a si
|
||||
missing procedure, check first whether an installed skill owns it and name that skill instead of
|
||||
transcribing it. See `references/contract.md`.
|
||||
|
||||
The delegation check is not a length brake — it fires only on procedure an invocable skill already
|
||||
owns, and says nothing about original prose. That brake is judgment, and it is the only one left:
|
||||
for every sentence you add, ask "would the agent get this wrong without it?" and delete it if the
|
||||
answer is no.
|
||||
|
||||
**Explain the why.** Reasoning-based instructions outperform rigid directives. A rule written in
|
||||
all caps (ALWAYS/NEVER) is usually better reframed as why the behaviour matters, so the agent can
|
||||
apply judgment at the edges.
|
||||
@@ -73,4 +78,10 @@ that was already there.
|
||||
If the edit adds or removes research-sourced content, update `source_keys` in the edited file and
|
||||
the matching `sources.md` entry — the create flow's Step 3 has the rules.
|
||||
|
||||
**Check for regressions before handing back.** `SKILL.md` Step 4 tells you to resolve every FAIL,
|
||||
which says nothing about a check that passed *before* these edits and no longer does. Compare the
|
||||
closing `agent-audit` against the agent's pre-edit state — a PASS that has become a SUGGESTION, or
|
||||
a SUGGESTION that has become a FAIL, is damage this flow caused and is in scope for it. Only the
|
||||
improve flow can make that comparison; the create flow has no prior state to compare against.
|
||||
|
||||
Then return to `SKILL.md` Step 4.
|
||||
@@ -37,6 +37,11 @@ The rule is about a field's *shape*, not a fixed roster:
|
||||
Claude Code honours it for plugin subagents; the three fields plugin agents do silently ignore
|
||||
are `hooks`, `mcpServers` and `permissionMode`, and this is not one of them. Copilot's handling
|
||||
of the key is unconfirmed, which ADR-0016 accepts as a stated risk.
|
||||
|
||||
Its syntax is the same at every scope, and this is the one scope that cannot reach it anywhere
|
||||
else: MCP tools are denied as `mcp__<server>`, `mcp__<server>__*` or `mcp__*`; both a YAML list
|
||||
and a delimited string are accepted, and this repo writes the comma-separated string form
|
||||
(`disallowedTools: Edit, Write, NotebookEdit`) — match it.
|
||||
- The Claude-only knobs (`isolation`, `maxTurns`, `effort`, `memory`, `permissionMode`, `skills`,
|
||||
`color`, `initialPrompt`, `background`, `hooks`, `mcpServers`) have no Copilot equivalent and
|
||||
are never written to this file at all. "Silently ignored at plugin scope" is the wrong framing:
|
||||
@@ -63,5 +68,7 @@ to installed skills instead of transcribed procedure.
|
||||
- [ ] No `FILL IN:` placeholder and no `<!-- ... -->` template comment anywhere in the file
|
||||
- [ ] System prompt body non-empty, and a read-only agent says so in prose as well as in
|
||||
`disallowedTools`
|
||||
- [ ] Body covers all four required elements: inputs expected, process steps, output format,
|
||||
**error handling** — what the agent does on malformed, missing or contradictory input
|
||||
|
||||
Then return to the flow reference you came from.
|
||||
@@ -25,7 +25,10 @@ duplicate silently.
|
||||
**`description`** — write it against `references/contract.md`. It is the primary signal for
|
||||
autonomous delegation.
|
||||
|
||||
**`tools`** — an allowlist; omit it to inherit every tool from the parent. Use `Agent(type1,type2)`
|
||||
**`tools`** — an allowlist. Write it, and restrict it to the tools the agent actually needs;
|
||||
omitting it inherits every tool from the parent, which is the right value only when the agent
|
||||
genuinely needs all of them. Least privilege is the default, not the exception. Use
|
||||
`Agent(type1,type2)`
|
||||
to restrict which subagent types this agent may spawn, and omit `Agent` entirely to stop it
|
||||
spawning any. Five tools reach no subagent whatever this field says — `AskUserQuestion`,
|
||||
`EnterPlanMode`, `ExitPlanMode`, `ScheduleWakeup` and `WaitForMcpServers` — so listing one buys
|
||||
@@ -95,6 +98,8 @@ Both files:
|
||||
|
||||
- [ ] `name` present and kebab-case; `description` written to `references/contract.md`
|
||||
- [ ] System prompt body present, non-empty and equivalent across the pair
|
||||
- [ ] Body covers all four required elements: inputs expected, process steps, output format,
|
||||
**error handling** — what the agent does on malformed, missing or contradictory input
|
||||
- [ ] No `FILL IN:` placeholder and no `<!-- ... -->` template comment left
|
||||
|
||||
Copilot file only:
|
||||
|
||||
@@ -285,6 +285,8 @@ else
|
||||
if [[ "$SCOPE" == "plugin" ]]; then
|
||||
echo " 1. Fill in $APM_FILE — replace every FILL IN: placeholder. Optional fields are" >&2
|
||||
echo " scaffolded there as commented blocks; uncomment the ones that apply." >&2
|
||||
echo " Description: 250 chars target / 400 ceiling (ADR-0020). The body has no" >&2
|
||||
echo " word gate — delegate to a skill instead of restating what it does." >&2
|
||||
echo " 2. Populate $SOURCES_DIR/sources.md with research sources, or delete it" >&2
|
||||
echo " 3. Validate: $VALIDATE_HINT $APM_FILE" >&2
|
||||
echo " It checks the frontmatter against the apm-agent-allowlist section of" >&2
|
||||
@@ -292,6 +294,8 @@ else
|
||||
else
|
||||
echo " 1. Fill in $CC_FILE — replace every FILL IN: placeholder. Optional fields are" >&2
|
||||
echo " scaffolded there as commented blocks; uncomment the ones that apply." >&2
|
||||
echo " Description: 250 chars target / 400 ceiling (ADR-0020). The body has no" >&2
|
||||
echo " word gate — delegate to a skill instead of restating what it does." >&2
|
||||
echo " 2. Fill in $CP_FILE — same, and heed its closing comment: the Claude Code-only" >&2
|
||||
echo " fields it names must not cross over from the file above." >&2
|
||||
echo " 3. Validate: run $VALIDATE_HINT on each file" >&2
|
||||
|
||||
@@ -6,10 +6,12 @@ Audit a skill directory against the agentskills.io specification and the house c
|
||||
|
||||
1. Runs `scripts/validate.sh` and `scripts/validate-provenance.sh` for structural and provenance checks, plus `scripts/vale-wrap.sh` — a Vale prefilter that deterministically flags non-imperative description openers, composition and architecture notes, vague wording, padding phrases, and "There is/are" sentence openers
|
||||
2. Reads all files in the skill directory
|
||||
3. Applies qualitative checks across six dimension groups, loading one rubric from `references/` per group
|
||||
3. Applies qualitative checks across five dimension groups, loading one rubric from `references/` per group
|
||||
4. Outputs a compact findings report — findings only, grouped by dimension, each with Why and Fix — and a result block with handoff to `skill-author`
|
||||
|
||||
`validate.sh` enforces two independent length families that must not be conflated: the agentskills.io spec conformance ceilings (500 lines, 2,770 words, both counting the whole file) and the ADR-0020 context budget (250/400 description characters, 600/900 body-only words, plus resolvable boundary targets).
|
||||
`validate.sh` enforces two independent length families that must not be conflated: the agentskills.io spec conformance ceilings (500 lines, 2,770 words, both counting the whole file) and the ADR-0020 context budget (250/400 description characters, 600/900 body-only words).
|
||||
|
||||
Alongside those it runs four shape checks that are not length measurements at all. Two are FAILs: every routing target named in the description — in the compressed `Not <thing> -> <name>` arrow **and** in the prose form — must resolve to a real skill or agent, and every `references/<file>.md` the body names must exist on disk. Three are SUGGESTIONs: a missing boundary clause, a Gotchas section over five entries, and a Gotchas section over 25% of the body. The resolution universe for boundary targets is derived by walking up from the audited `SKILL.md` — the authoring root above it, its own apm package, and that package's declared `apm.yml` dependencies — so a fresh clone and a machine that has run `apm install` return the same verdict. When no universe can be determined the check prints `INFO ... DID NOT RUN` and does not silently pass.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -24,7 +26,7 @@ Provide the path to the skill directory to audit when invoking.
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `SKILL.md` | Skill instructions for agents |
|
||||
| `scripts/validate.sh` | Structural validator — checks name format, name matches directory, description length, line count, placeholder detection, script executable bit, and interactive-prompt detection |
|
||||
| `scripts/validate.sh` | Structural validator — checks name format, name matches directory, description presence and length, body-only word count, line and whole-file word ceilings, boundary-clause presence, boundary-target resolution, `references/` pointer existence, Gotchas entry count and body share, placeholder detection, script executable bit, and interactive-prompt detection |
|
||||
| `scripts/validate-provenance.sh` | Provenance validator — checks sources.md completeness, source_keys/slug consistency, Contributing files existence, bidirectional linkage, Research doc: fields, and upstream research doc alignment |
|
||||
| `scripts/vale-wrap.sh` | Vale prefilter wrapper — runs the bundled `Kyberforge` Vale styles against SKILL.md and reports alerts as deterministic FAILs ahead of Step 3's qualitative review |
|
||||
| `assets/vale/.vale.ini` | Vale configuration — points Vale at the bundled `Kyberforge` style path, self-located relative to `vale-wrap.sh` |
|
||||
@@ -38,6 +40,7 @@ Provide the path to the skill directory to audit when invoking.
|
||||
| `references/patterns.md` | Rubric for the patterns dimension — which instruction construct fits which job, and how each is correctly formed |
|
||||
| `references/file-structure.md` | Rubric for the file-structure and internal-consistency dimensions — permitted directories, cross-plugin path rules and their two structural exemptions, README drift |
|
||||
| `references/formatting-and-scripts.md` | Rubric for the formatting and scripts dimensions — heading and fencing conventions, and the agentic-use criteria for bundled scripts |
|
||||
| `references/validation-scripts.md` | Step 1 troubleshooting — the manual structural fallback when `validate.sh` cannot run, and the script exit codes that are easy to misread (loaded only on a script failure) |
|
||||
| `references/sources.md` | Provenance record — agentskills.io sources that informed this skill and which files each contributed to |
|
||||
| `tests/validate.bats` | (source-only) Bats test suite for validate.sh |
|
||||
| `tests/validate-provenance.bats` | (source-only) Bats test suite for validate-provenance.sh |
|
||||
|
||||
@@ -33,7 +33,9 @@ bash scripts/validate-provenance.sh <skill-dir>
|
||||
scripts/vale-wrap.sh <skill-dir>/SKILL.md
|
||||
```
|
||||
|
||||
`validate.sh` findings become the `### Structure` dimension — its FAILs and its SUGGESTIONs both. If it cannot run at all (no `python3`, Bash denied), report that as an INFO finding rather than guessing; what it measures is not reproducible by reading.
|
||||
`validate.sh` findings become the `### Structure` dimension — its FAILs and its SUGGESTIONs both.
|
||||
|
||||
If any of the three cannot run, or exits non-zero for a reason other than findings, read `references/validation-scripts.md` — it carries the manual fallback and the misleading exit codes. Ordinary content FAILs are the expected outcome here and need no fallback.
|
||||
|
||||
`validate-provenance.sh` prints nothing on success. Its FAIL and INFO findings become a separate `### Provenance` dimension, and it emits Why and Fix itself — surface those verbatim.
|
||||
|
||||
|
||||
@@ -28,8 +28,10 @@ Include content the agent lacks:
|
||||
- The specific tools or sequences to use — not the full range of options
|
||||
- One default per decision point with one escape hatch
|
||||
|
||||
Move to `references/`, behind an explicit "If X, read `references/file.md`" trigger — the literal
|
||||
conditional form, never a generic pointer:
|
||||
Move to `references/`, behind an explicit "If X, read `references/<file>.md`" trigger — the literal
|
||||
conditional form, never a generic pointer. Write the real filename in the skill under audit; the
|
||||
angle brackets are a placeholder here, and a literal `references/file.md` in a body is an ERROR
|
||||
from the ADR-0020 gate because no such file exists on disk. Move:
|
||||
|
||||
- Lookup tables and spec restatements
|
||||
- Output schemas, templates and example blocks
|
||||
@@ -69,8 +71,10 @@ table** plus the gates common to every branch, and each flow lives in its own se
|
||||
`references/` file. Inlining all of them is a FAIL regardless of word count, because every
|
||||
invocation then pays for every branch it did not take.
|
||||
|
||||
The reference shape in this repo is `apm-workflow`: a 554-word body dispatching to roughly 3,000
|
||||
words of references across five mutually exclusive invocations.
|
||||
The reference shape in this repo is `apm-workflow`: a **421-word body** dispatching to roughly
|
||||
3,000 words of references across five mutually exclusive invocations. Its whole-file count is 554
|
||||
words — cite 421 when calibrating a body, or the conflation this section warns against reappears
|
||||
in the finding itself.
|
||||
|
||||
## Gotchas sections
|
||||
|
||||
@@ -86,17 +90,20 @@ sensibly.
|
||||
|
||||
Constraints:
|
||||
|
||||
- **Maximum five entries.** Past five, the section is a summary of the body rather than a set of
|
||||
traps, and the agent stops reading it as a warning.
|
||||
- **More than five entries is a SUGGESTION** — five is the guideline, not a ceiling. Past five, the
|
||||
section is usually a summary of the body rather than a set of traps, and the agent stops reading
|
||||
it as a warning. It stays advisory because whether a given gotcha earns its place is judgment;
|
||||
`validate.sh` emits it through `suggest()` and the run still exits 0.
|
||||
- **A Gotcha that paraphrases a step in the body below it is a FAIL.** It has no independent
|
||||
content, and it teaches the agent that Gotchas can be skimmed because the real instruction is
|
||||
coming.
|
||||
coming. This one is the auditor's call — no script detects it.
|
||||
- **A Gotchas section exceeding 25% of the body is a SUGGESTION** — the body has been inverted into
|
||||
a preamble.
|
||||
a preamble. Same tier and same reasoning as the entry count, and independent of it: either can
|
||||
fire without the other.
|
||||
- Place the section near the top. A gotcha read after the mistake is worthless, which is also why
|
||||
Gotchas is the one construct exempt from moving to `references/`.
|
||||
|
||||
Worked negative example — `git-commits` carries thirteen entries, of which four restate content
|
||||
Worked negative example — `git-commits` carries twelve entries, of which four restate content
|
||||
that already appears below or in the description:
|
||||
|
||||
| Gotcha | Restates |
|
||||
@@ -106,8 +113,10 @@ that already appears below or in the description:
|
||||
| `:33` "Never skip hooks with `--no-verify`" | step 9 at `:52` |
|
||||
| `:36` "Never commit secrets" | step 2 at `:45` |
|
||||
|
||||
All four are FAILs under this rule, and the section as a whole breaches the five-entry maximum. It
|
||||
also passes every plausible word gate, which is the point of auditing the construct directly.
|
||||
All four are FAILs under the paraphrase rule. The entry count and the section's share of the body
|
||||
(387 of 1,102 words, 35%) are two further SUGGESTIONs on top — the script reports both, and neither
|
||||
fails the run on its own. What makes this worth auditing directly is that the four paraphrase FAILs
|
||||
pass every word gate there is; only reading the construct finds them.
|
||||
|
||||
## Calibrating control
|
||||
|
||||
@@ -144,7 +153,7 @@ Flag as FAIL if:
|
||||
- A sentence answers "no" to the core test — it is padding
|
||||
- The body exceeds 900 words counted body-only (`validate.sh` reports it)
|
||||
- Two or more mutually exclusive flows are inlined instead of dispatched
|
||||
- A Gotcha paraphrases a step in the body below it, or the section exceeds five entries
|
||||
- A Gotcha paraphrases a step in the body below it
|
||||
- A decision point presents a menu of options with no default
|
||||
- An instruction repeats content already in the description
|
||||
- A prescriptive sequence is used where flexibility is fine, or the reverse
|
||||
@@ -152,6 +161,7 @@ Flag as FAIL if:
|
||||
Flag as SUGGESTION if:
|
||||
|
||||
- The body exceeds 600 words counted body-only but stays at or under 900
|
||||
- The Gotchas section carries more than five entries
|
||||
- The Gotchas section exceeds 25% of the body
|
||||
- A rationale is missing from an include/exclude rule — present but unexplained
|
||||
- Gotchas are correct but placed late in the body rather than near the top
|
||||
|
||||
@@ -28,6 +28,14 @@ resolving there. Flag any `../`, `../../`, or absolute repo path (`plugins/<plug
|
||||
and its APM-native equivalent `.apm/skills/<other>/`) appearing in `SKILL.md`, `scripts/`,
|
||||
`references/` or `assets/`.
|
||||
|
||||
**Referring to another skill's file.** There is one sanctioned spelling, and it is possessive:
|
||||
`skill-audit's references/validation-scripts.md`. Write the skill by name and let the reader
|
||||
resolve it — do not spell the repo path. The full path is the thing this section forbids, and
|
||||
`references/validation-scripts.md` on its own is a hard ERROR from the ADR-0020 gate, which
|
||||
requires an unqualified `references/` pointer to exist in the skill's OWN directory. The
|
||||
possessive form is the only spelling both rules accept; the gate recognises it and skips the
|
||||
on-disk check. Flag any other spelling of a cross-skill reference.
|
||||
|
||||
Two directories are exempt, and the exemptions are structural rather than discretionary:
|
||||
|
||||
- **`references/sources.md`.** Its `Research doc:` fields are development-time provenance pointers,
|
||||
|
||||
@@ -32,8 +32,16 @@ read after the mistake.
|
||||
inner fence as `` \`\`\` ``. An unescaped inner fence terminates the outer block and the remaining
|
||||
instructions render as prose.
|
||||
|
||||
**Conditional references** state a specific trigger: "If the API returns a non-200 status, read
|
||||
`references/api-errors.md`." The generic form — pointing at the directory and hoping — defeats
|
||||
**Conditional references** state a specific trigger, naming a file that exists in the skill's own
|
||||
`references/` directory:
|
||||
|
||||
```text
|
||||
If the API returns a non-200 status, read `references/api-errors.md`.
|
||||
```
|
||||
|
||||
That block is fenced because the filename in it is illustrative — an unfenced `references/` pointer
|
||||
in a `SKILL.md` body must resolve on disk or the ADR-0020 gate reports a hard ERROR. The generic
|
||||
form — pointing at the directory and hoping — defeats
|
||||
progressive disclosure, because the agent either loads everything or loads nothing.
|
||||
`Kyberforge.PaddingPhrase` catches the common generic phrasing deterministically; other malformed
|
||||
forms are judgment.
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
- **URL:** https://agentskills.io/specification.md
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
|
||||
- **Description:** Complete SKILL.md format specification — frontmatter fields, constraints, body content, optional directories, progressive disclosure levels, file references, validation
|
||||
- **Contributing files:** SKILL.md, references/body-discipline.md, references/description-quality.md, references/patterns.md, references/file-structure.md, references/formatting-and-scripts.md
|
||||
- **Contributing files:** SKILL.md, references/body-discipline.md, references/description-quality.md, references/patterns.md, references/file-structure.md, references/formatting-and-scripts.md, references/validation-scripts.md
|
||||
- **Status:** `extracted`
|
||||
|
||||
## agentskills-best-practices
|
||||
@@ -47,7 +47,7 @@
|
||||
- **URL:** https://agentskills.io/skill-creation/using-scripts.md
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
|
||||
- **Description:** Using scripts in skills — one-off commands, self-contained scripts with inline dependencies, designing scripts for agentic use (no interactive prompts, --help, structured output, idempotency)
|
||||
- **Contributing files:** SKILL.md, references/formatting-and-scripts.md
|
||||
- **Contributing files:** SKILL.md, references/formatting-and-scripts.md, references/validation-scripts.md
|
||||
- **Status:** `extracted`
|
||||
|
||||
## agentskills-quickstart
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
source_keys:
|
||||
- agentskills-spec
|
||||
- agentskills-using-scripts
|
||||
---
|
||||
|
||||
# Validation Scripts Reference
|
||||
|
||||
Read this when a Step 1 script fails, cannot run, or reports something that needs interpreting.
|
||||
Nothing here is needed on a clean run.
|
||||
|
||||
## Report the gap, do not guess
|
||||
|
||||
If a script cannot run at all — Bash denied, `python3` unavailable, PyYAML not importable, `vale`
|
||||
not installed — say so as an **INFO** finding naming the script and the missing dependency, then
|
||||
fall back to the manual checks below. An INFO never changes PASS/FAIL. Silently omitting the
|
||||
dimension a script would have covered reports a clean audit that checked less than it claims to
|
||||
have checked, and the Step 4 coverage line then names a dimension nothing actually examined.
|
||||
|
||||
## Manual structural fallback
|
||||
|
||||
`validate.sh` needs `python3` **and** PyYAML, and refuses to start without either — the description
|
||||
value has to be measured after YAML folding is resolved, so skipping the ADR-0020 gates would be a
|
||||
vacuous pass rather than a partial one. The two are checked separately, so the message already names
|
||||
the right one — report it verbatim rather than diagnosing further:
|
||||
|
||||
```text
|
||||
Error: python3 is required but was not found on PATH.
|
||||
Error: PyYAML is required but is not importable by python3.
|
||||
```
|
||||
|
||||
Without them — or with Bash denied, or on a permission error — work this list
|
||||
by hand and file the results under `### Structure` exactly as the script's output would have been:
|
||||
|
||||
- **`name`** present, 1–64 characters, kebab-case (lowercase letters, digits and hyphens; no
|
||||
leading, trailing or doubled hyphen), and **matching the skill's directory name** exactly.
|
||||
- **`description`** present and non-empty; no unfilled `FILL IN:` placeholder in it. An absent or
|
||||
empty description is a **FAIL**, never a silent skip — it is the one field preloaded into every
|
||||
session, so a skill without one can never be routed to.
|
||||
- **Description length**, measured on the folded YAML value with newlines collapsed to single
|
||||
spaces — not on the raw block scalar, which counts indentation. 250 characters SUGGESTION, 400
|
||||
FAIL (ADR-0020), 1,024 FAIL (agentskills.io spec).
|
||||
- **Body length**, counting everything after the frontmatter's closing `---`. 600 words
|
||||
SUGGESTION, 900 FAIL (ADR-0020).
|
||||
- **Whole-file ceilings**, counting the file including frontmatter: 500 lines FAIL, 2,770 words
|
||||
FAIL (agentskills.io spec). These are a different measurement from the two above — report them
|
||||
as separate findings, never merged.
|
||||
- **A boundary clause is present** — either the prose form (`do not` / `instead` / `rather than` /
|
||||
`not for`) or ADR-0020's compressed `Not <thing> -> <name>` arrow. **SUGGESTION**, not FAIL:
|
||||
the absence is deterministic, but whether this skill warrants one is the auditor's call.
|
||||
- **Boundary targets resolve** — **FAIL** on a name that resolves to nothing. See the section
|
||||
below; resolving these by hand is the one item on this list with a procedure of its own.
|
||||
- **Every `references/<file>.md` named in the body exists on disk** — **FAIL**, not a suggestion.
|
||||
A dispatch table or "read X" trigger naming a missing file sends the agent nowhere. Ignore
|
||||
mentions inside fenced code blocks, and ignore a mention whose own line says the file is gone
|
||||
(`removed`, `deleted`, `renamed`, `superseded`, `replaced`, `obsolete`, `deprecated`, `former`,
|
||||
`gone`, `no longer`, `used to`) — that is a historical note, not a dispatch entry.
|
||||
- **Gotchas discipline**, both **SUGGESTION**. Locate the section by a heading that *is* Gotchas
|
||||
(`## Common Gotchas` counts; `## Gotcha handling` and `## Why gotchas matter` do not), running to
|
||||
the next heading at the same level or shallower. More than five top-level entries is one
|
||||
suggestion; a section over 25% of the body word count is a second, independent one. Count
|
||||
entries at column 0 only — an indented child bullet is not an entry — and ignore fenced code
|
||||
blocks for both.
|
||||
- **No unfilled `FILL IN:` placeholder** anywhere in the body.
|
||||
- **Every file in `scripts/`** carries the executable bit and contains no interactive prompt —
|
||||
no bare `read`, no `select`, nothing that blocks on a TTY.
|
||||
|
||||
## Resolving boundary targets by hand
|
||||
|
||||
Targets are read from **both** boundary forms. The compressed `Not <thing> -> <name>` arrow and the
|
||||
prose form are each parsed *and* target-checked, so a typo in prose phrasing fails exactly as an
|
||||
arrow typo does — do not check only the names after an arrow.
|
||||
|
||||
Build the universe by walking up **from the `SKILL.md` under audit**, never from the validator's own
|
||||
location. The nearest ancestor holding `plugins/*/.apm/skills/` or `plugins/*/.apm/agents/` is the
|
||||
authoring root, falling back to the nearest ancestor holding `.git`. When one is found the universe
|
||||
is every skill and agent under `<root>/plugins/*/`, plus the skill's own apm package, plus the
|
||||
packages that package declares in its `apm.yml` under `dependencies.apm`. Deployed `.claude/` and
|
||||
`.agents/` trees are consulted **only** when no authoring root exists — they are gitignored
|
||||
`apm install` output, and reading them would make a fresh clone and a developer machine disagree.
|
||||
|
||||
Three ways to read the result wrong:
|
||||
|
||||
- **A hyphenated name used attributively is not a dangling target.** "Use pre-commit hooks instead
|
||||
of ad-hoc scripts" reads as a route to `pre-commit` on wording alone. What separates a route from
|
||||
prose is grammar: a route target is terminal — followed by punctuation, a conjunction, or a
|
||||
boundary word — whereas a compound modifier is followed by the noun it modifies. A name followed
|
||||
by an ordinary noun still *confirms* a route when it exists, but never raises a FAIL on its own.
|
||||
- **A SUGGESTION-tier unresolved target is not a FAIL you may promote.** Terminal position alone is
|
||||
not evidence of a route: "run `pre-commit` instead", "see `commit-msg`" and "use the clean-up
|
||||
instead" are all terminal and all prose. A prose-form target earns a FAIL only when its own
|
||||
sentence names another target that *does* resolve; otherwise the script reports it and moves on,
|
||||
and so should you. Route notation — `/name` and `-> name` — is exempt and always FAILs, and it is
|
||||
the fix to recommend when the author did mean a route.
|
||||
- **`INFO boundary-target resolution DID NOT RUN` is not a pass.** The script prints it, and exits
|
||||
0, when no universe could be determined for that path — the usual cause being a skill copy
|
||||
audited outside its package. Report it as an INFO naming the unchecked targets and re-run against
|
||||
the real directory; filing it as clean signs off targets nothing verified.
|
||||
|
||||
## Script-specific failures
|
||||
|
||||
- **`validate-provenance.sh` printed nothing.** That is a pass, not a skip. It also exits 0
|
||||
silently when the skill has no `source_keys` and no `references/sources.md` — nothing to
|
||||
validate is not a finding.
|
||||
- **`vale` reports `0 files`.** Treat the pass as NOT RUN, not as clean, and fall back to full
|
||||
Step 3 judgment for the dimensions it would have covered. The bundled `Kyberforge` style is
|
||||
scoped by glob in `assets/vale/.vale.ini`; a file outside those globs is silently not linted.
|
||||
- **`E100 Runtime error ... does not exist` (exit 2) from `vale-wrap.sh`.** An explicit relative
|
||||
`--config` was passed. Pass none: the wrapper locates its own `assets/vale/.vale.ini` from its
|
||||
own path, so a resolved script path plus an unresolved config path produces exactly this. Do not
|
||||
read this exit code as vale being unavailable — that misreading sends the audit down the
|
||||
fallback path while vale was installed and working the whole time.
|
||||
- **The `vale` binary is genuinely absent** (`command not found`). Report one INFO naming it, then
|
||||
fall back to full Step 3 judgment for the description, body-discipline and patterns dimensions —
|
||||
the prefilter's whole coverage. Judge those by rubric rather than dropping them.
|
||||
- **A path argument that does not exist is a hard error** in `vale-wrap.sh`, deliberately: bare
|
||||
`vale` would fall back to reading stdin and print a clean-looking `0 errors ... in stdin`, which
|
||||
the `0 files` guard above does not catch.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -47,6 +47,7 @@ If the destination resolves inside an APM package, read `references/deployment-m
|
||||
| `references/create.md` | The create flow end to end — prerequisites, package-intent gate, scaffold, frontmatter, scripts, references, sources (loaded on demand) |
|
||||
| `references/improve.md` | The improve flow end to end — signal verification, root-cause grouping, announcement, edits (loaded on demand) |
|
||||
| `references/contract.md` | The ADR-0020 description and body contract, the Gotchas constraint, the two size gates, body patterns, and org-policy embedding (loaded on demand) |
|
||||
| `references/retrofit.md` | Bringing a pre-ADR-0020 skill into contract — ordered cut procedure, the mutually-exclusive-flows test, reference-file conventions, the collateral checklist, and a worked description retrofit (loaded from the improve flow when a budget is exceeded) |
|
||||
| `references/deployment-modes.md` | APM package vs standalone differences and self-containment/cache-isolation rules (loaded on demand) |
|
||||
| `references/scripts.md` | Package runners, inline dependency patterns, and full script contract (loaded on demand) |
|
||||
| `references/sources.md` | Upstream research sources and which skill files each contributed to |
|
||||
|
||||
@@ -19,10 +19,9 @@ metadata:
|
||||
|
||||
## Gotchas
|
||||
|
||||
- A skill's `name` and `description` are preloaded into every agent's context every session, invoked or not; the body loads only on invocation. The description is the scarce budget.
|
||||
- The word gates are two different measurements, not one rule with two tiers. The 2,770-word / 500-line spec backstop counts the whole file including frontmatter; Step 3's gate counts the body alone. A file can sit well inside one and fail the other, so never unify them.
|
||||
- Never spawn a subagent to audit or recheck your own work here. Run `/skill-audit` inline, in the same context as the edits. Clean-context recheck belongs to `/forge`'s outer loop, and a self-spawned subagent can have its worktree torn down by concurrent cleanup, destroying an uncommitted draft.
|
||||
- Do not create new scripts unless a signal explicitly calls for it. Writing one from scratch requires transcript analysis that is out of scope here — flag the opportunity as a suggestion instead.
|
||||
- The word gates are two measurements, not two tiers of one rule: the 2,770-word / 500-line spec backstop counts the whole file, Step 3's gate the body alone. Never unify them.
|
||||
- Never spawn a subagent to audit or recheck your own work — run `/skill-audit` inline, in the same context as the edits. Clean-context recheck belongs to `/forge`'s outer loop, and a self-spawned subagent's worktree can be torn down by concurrent cleanup, destroying an uncommitted draft.
|
||||
- Do not create new scripts unless a signal explicitly calls for it. Writing one from scratch requires out-of-scope transcript analysis — flag the opportunity as a suggestion instead.
|
||||
|
||||
## Step 1 — Dispatch
|
||||
|
||||
@@ -32,15 +31,15 @@ metadata:
|
||||
| Directory exists, at least one improvement signal present | Improve | `references/improve.md` |
|
||||
| Directory exists, no signals | Stop and ask | — |
|
||||
|
||||
Signals: grill output, `/skill-audit` findings, inline feedback, eval results, session context describing what went wrong. With none, ask: "No improvement signals found. Did you mean to create a new skill, or do you have feedback to apply?"
|
||||
Signals: grill output, `/skill-audit` findings, inline feedback, eval results, session context describing what went wrong. With none, ask whether the user meant to create a new skill or has feedback to apply.
|
||||
|
||||
Read only the reference matching the resolved flow — each is self-contained. Capture `git log --oneline -1` before touching the filesystem; Step 4 needs it.
|
||||
Read only the reference matching the resolved flow — each is self-contained. If the target sits inside a git worktree, capture `git log --oneline -1` before touching the filesystem; Step 4 needs it.
|
||||
|
||||
## Step 2 — Invocation axis
|
||||
|
||||
Decide before writing any description: model-invoked or hand-invoked?
|
||||
|
||||
- **Hand-invoked** — the user types `/name` and no agent should route to it. Set `disable-model-invocation: true` and write one plain human-facing sentence: no trigger list, no boundary clause. Worked example: `plugins/bin/.apm/skills/zoom-out/SKILL.md`. Skip Step 3's description rules.
|
||||
- **Hand-invoked** — the user types `/name` and no agent should route to it. Set `disable-model-invocation: true` and write one plain human-facing sentence: no trigger list, no boundary clause. Skip Step 3's description rules.
|
||||
- **Model-invoked** — the default.
|
||||
|
||||
## Step 3 — Contract
|
||||
@@ -51,12 +50,12 @@ Gates `/skill-audit` enforces in both flows:
|
||||
|
||||
- **Description** — a trigger clause, at most one capability clause, and a boundary clause shaped `Not <thing> -> <skill-name>` whose target resolves to a real skill or agent. 250 characters SUGGESTION, 400 FAIL, value only.
|
||||
- **Body** — decision procedure only: ordered steps, branches, gates, and which reference to load when. 600 words SUGGESTION, 900 FAIL, body only. At two or more mutually exclusive flows a dispatch table is mandatory and each flow gets its own self-contained `references/` file.
|
||||
- **Gotchas** — at most five, each contradicting a reasonable default. A Gotcha paraphrasing a step below it is a FAIL.
|
||||
- **Gotchas** — each contradicting a reasonable default. A Gotcha paraphrasing a step below it is a FAIL; over five entries is a SUGGESTION only.
|
||||
|
||||
## Step 4 — Validate and close
|
||||
|
||||
Run `/skill-audit` on the resolved skill directory. It checks name-to-directory match, description presence, leftover `FILL IN:` placeholders, both size budgets, boundary-target resolution and script hygiene — do not hand-check those first. Resolve every FAIL before reporting done.
|
||||
Run `/skill-audit` on the resolved skill directory; resolve every FAIL before reporting done. It checks name-to-directory match, placeholders, both size budgets, boundary-target resolution and script hygiene — do not hand-check those. Hand-check the one thing it misses: an empty body reports `PASS SKILL.md body word count 0 (ADR-0020 target: 600)`, so confirm at least one non-empty section exists.
|
||||
|
||||
With `metadata.version` present, bump the **minor** version on create (new skills start at `0.1.0`) and the **patch** version on improve.
|
||||
|
||||
**Commit verification.** Once the audit is clean, run `git add` and `git commit` — do not stop at staging. Re-run `git log --oneline -1` and confirm the hash changed from the one captured at Step 1. A non-empty `git diff --stat` is not proof: staged-but-uncommitted work is part of no commit and is silently lost if the tree is cleaned up first. Report done only once the hash has changed.
|
||||
**Commit verification.** Inside a git worktree: once the audit is clean, run `git add` and `git commit` — do not stop at staging. Re-run `git log --oneline -1` and confirm the hash changed from Step 1's. A non-empty `git diff --stat` is not proof: staged-but-uncommitted work is part of no commit and is silently lost if the tree is cleaned up. Report done only once the hash has changed. Outside a worktree (a skill under `~/.claude/skills/`, say) nothing is committable — report done on a clean audit, naming that as the reason.
|
||||
@@ -10,14 +10,22 @@ name: SKILL_NAME
|
||||
# Examples: my-tool, data-analyzer, pdf-processor
|
||||
|
||||
description: >
|
||||
Use when FILL IN: trigger — when should an agent activate this skill?
|
||||
Use when FILL IN: trigger.
|
||||
FILL IN: at most ONE capability clause, stated specifically
|
||||
(e.g. "parses and validates OpenAPI specs", not "helps with APIs").
|
||||
Not FILL IN: near-miss case -> FILL IN: real sibling skill name.
|
||||
Not FILL IN: near-miss case -> FILL IN: real sibling skill.
|
||||
# Required. Preloaded into EVERY session whether or not the skill is invoked.
|
||||
# Exactly three parts, in this order: trigger clause, at most one capability
|
||||
# clause, boundary clause. Drop the boundary line if no near-miss skill exists.
|
||||
# Budget: 250 characters target, 400 hard ceiling (counting this value only).
|
||||
# Trigger clause: when should an agent activate this skill? Describe the user's
|
||||
# intent, not the skill's internal mechanics.
|
||||
# Budget: 250 characters target, 400 hard ceiling (counting this value only,
|
||||
# with YAML folding resolved). This scaffold sits at 214 — keep the fill-in
|
||||
# under the target rather than growing past it.
|
||||
# Boundary clauses may be plural: write one per genuine near-miss, and none
|
||||
# where no sibling could steal activations.
|
||||
# Never let a hyphenated skill name wrap across two lines of this folded block
|
||||
# — folding turns the break into a space and the routing target stops resolving.
|
||||
# Banned here: capability lists, output-format detail, composition notes,
|
||||
# implementation detail, and restating one trigger twice in two registers.
|
||||
# The boundary target must resolve to a real skill or agent — it is checked.
|
||||
|
||||
@@ -47,11 +47,21 @@ explicitly" only where the user's natural phrasing genuinely omits the domain wo
|
||||
for `git-commits`, where the user says "commit". Adding one everywhere is what inflated this
|
||||
corpus, and it was deleted as a blanket rule.
|
||||
|
||||
**Boundary targets must resolve.** The name after the arrow is checked against real skill
|
||||
directories under `plugins/*/.apm/skills/<name>/` and real agents under
|
||||
`plugins/*/.apm/agents/<name>.agent.md`. A boundary clause naming a target that does not exist
|
||||
sends the router nowhere and fails the audit. Check the target exists before writing it — do not
|
||||
invent a plausible sibling name.
|
||||
**Boundary targets must resolve.** Both forms are checked — the arrow and the prose form ("do not
|
||||
use for X, use `y` instead") — so a typo dangles either way. Targets resolve against a universe
|
||||
built by walking up **from the SKILL.md itself**: the nearest ancestor holding
|
||||
`plugins/*/.apm/{skills,agents}` (or, failing that, the nearest ancestor holding `.git`) contributes
|
||||
every skill and agent under `<root>/plugins/*/`, plus the skill's own apm package and the packages
|
||||
that package declares in `apm.yml` under `dependencies.apm`. A sibling plugin in the same monorepo
|
||||
therefore resolves; a skill in an unrelated repo does not. A boundary clause naming a target
|
||||
outside that universe sends the router nowhere and fails the audit. Check the target exists before
|
||||
writing it — do not invent a plausible sibling name.
|
||||
|
||||
That universe is the apm marketplace and stops there. A **host built-in is not a routing target**:
|
||||
`/compact`, `/clear` and `/init` are Claude Code slash commands with no counterpart in Copilot CLI
|
||||
or Codex, and `.apm/` source compiles for all three, so routing to one is a portability defect. The
|
||||
gate is right to fail it and there is no allowlist. If a built-in genuinely needs mentioning, write
|
||||
it un-slashed — ``the `compact` built-in`` — which makes no routing claim and is not checked.
|
||||
|
||||
**Length.** 250 characters SUGGESTION, 400 characters FAIL, counting the frontmatter value only
|
||||
with YAML folding resolved. The agentskills.io 1,024-character spec limit is unchanged and sits
|
||||
@@ -61,7 +71,13 @@ as the outlier stop.
|
||||
**Hand-invoked skills are exempt.** A skill carrying `disable-model-invocation: true` is absent
|
||||
from the model-visible listing and is reached only by the user typing `/name`. It takes one plain
|
||||
human-facing sentence — no trigger clause, no boundary clause, no indirect triggers. Worked
|
||||
example: `plugins/bin/.apm/skills/zoom-out/SKILL.md`.
|
||||
example — the whole description of the `zoom-out` skill, which carries `disable-model-invocation`:
|
||||
|
||||
````markdown
|
||||
Tell the agent to zoom out and give broader context or a higher-level perspective. Use when
|
||||
you're unfamiliar with a section of code or need to understand how it fits into the bigger
|
||||
picture.
|
||||
````
|
||||
|
||||
## Body
|
||||
|
||||
@@ -90,6 +106,12 @@ blocks, rationale prose, and any content only one branch reaches. Each reference
|
||||
self-contained for its concern, and every one is wired from the body with the literal conditional
|
||||
form:
|
||||
|
||||
**The one exception, stated once so it is not re-litigated:** an output schema stays in the body
|
||||
only when it applies to *every* flow and is short — roughly 50 words or less, which is the "Output
|
||||
format template" pattern below. An output schema that is longer than that, or that only one flow
|
||||
produces, moves to `references/` like any other schema. No third option exists, and the two rules
|
||||
do not disagree.
|
||||
|
||||
````markdown
|
||||
If <condition>, read `references/<file>.md`.
|
||||
````
|
||||
@@ -98,8 +120,9 @@ A generic pointer ("see references/ for details") is a Vale error — the agent
|
||||
|
||||
**Dispatch is mandatory at two or more mutually exclusive flows.** The body carries the dispatch
|
||||
table and the gates common to every branch; each flow gets its own self-contained `references/`
|
||||
file. Exemplar: `plugins/kyberforge/.apm/skills/apm-workflow/SKILL.md` — a 554-word body
|
||||
dispatching to 3,006 words of references.
|
||||
file. Exemplar: the `apm-workflow` skill — a **421-word body** dispatching to 3,006 words of
|
||||
references. Calibrate against 421: that file's whole-file count is 554 words, and aiming at that
|
||||
number instead overshoots the body budget by ~30%.
|
||||
|
||||
**Length.** 600 words SUGGESTION, 900 words FAIL, counting the **body only** — everything after
|
||||
the frontmatter's closing `---`.
|
||||
@@ -108,7 +131,7 @@ the frontmatter's closing `---`.
|
||||
|
||||
- Each entry must state a fact that **contradicts a reasonable default** — something the agent
|
||||
gets wrong by acting sensibly. "Never commit secrets" is not one; the agent already knows.
|
||||
- Maximum five entries.
|
||||
- More than five entries is a SUGGESTION — five is the guideline, not a ceiling.
|
||||
- A Gotcha that paraphrases a step in the body below it is a **FAIL**. If the rule is already a
|
||||
step, it is not a gotcha.
|
||||
- A Gotchas section exceeding 25% of the body is a SUGGESTION.
|
||||
@@ -164,7 +187,9 @@ Do not modify flags.
|
||||
| <condition> | <flow> | `references/<file>.md` |
|
||||
````
|
||||
|
||||
**Output format template** (when the skill produces structured output):
|
||||
**Output format template** (when the skill produces structured output on *every* flow, and the
|
||||
schema is roughly 50 words or less — see the exception under Body above; anything longer or
|
||||
flow-specific belongs in `references/`):
|
||||
|
||||
````markdown
|
||||
Output format:
|
||||
@@ -173,7 +198,8 @@ Output format:
|
||||
```
|
||||
````
|
||||
|
||||
For longer templates, place them in `assets/<name>.md` and reference conditionally.
|
||||
For longer templates, place them in `references/<topic>.md` or `assets/<name>.md` and reference
|
||||
conditionally.
|
||||
|
||||
## Embedding org-specific policy
|
||||
|
||||
|
||||
@@ -136,10 +136,16 @@ If no scripts are needed, delete `scripts/README.md` and the `scripts/` director
|
||||
|
||||
## Step 5 — Add references, assets, and tests (if needed)
|
||||
|
||||
**`references/`** — additional documentation loaded on demand. One topic per file. Reference
|
||||
conditionally from SKILL.md with the literal form ``If <condition>, read `references/<file>.md` ``.
|
||||
Keep reference chains one level deep — a reference file that references another reference file is
|
||||
rarely loaded correctly.
|
||||
**`references/`** — additional documentation loaded on demand. One topic per file, named in
|
||||
kebab-case after the topic. Reference conditionally from SKILL.md with the literal form
|
||||
``If <condition>, read `references/<file>.md` ``.
|
||||
|
||||
**Two hops from `SKILL.md`, never three.** A flow file may route on to a shared contract or
|
||||
sub-topic file — that is the shipped pattern here (`SKILL.md` → `references/create.md` → this
|
||||
file's own pointers to `contract.md`, `scripts.md` and `deployment-modes.md`). What does not work
|
||||
is a third hop: a file reachable only through two intermediates is rarely loaded at the moment it
|
||||
is needed. Every hop past the first also needs the same literal conditional form, so the agent
|
||||
knows when to take it.
|
||||
|
||||
**`assets/`** — static resources: templates, schemas, lookup tables. Reference by relative path
|
||||
from SKILL.md.
|
||||
|
||||
@@ -76,7 +76,19 @@ contract first — the gates are hot and carry no baseline file, so a one-line f
|
||||
non-compliant skill cannot be committed until the description and body meet
|
||||
`references/contract.md`. Treat that retrofit as part of the same change, not a follow-up.
|
||||
|
||||
If the skill's description exceeds 250 characters, or its body-only word count exceeds 600, read
|
||||
`references/retrofit.md` before editing. It carries the ordered cut procedure, the
|
||||
mutually-exclusive-flows test, the reference-file conventions this flow needs, the collateral
|
||||
checklist for `README.md` and `references/sources.md`, and a worked description retrofit. Do not
|
||||
improvise the cuts — four dry runs invented six to ten different answers to the same questions.
|
||||
|
||||
If a signal points to a script or reference file, edit that file directly rather than adding a
|
||||
workaround in SKILL.md.
|
||||
|
||||
**Check for regressions before handing back.** `SKILL.md` Step 4 tells you to resolve every FAIL,
|
||||
which says nothing about a check that passed *before* these edits and no longer does. Compare the
|
||||
closing audit against the skill's pre-edit state — a PASS that has become a SUGGESTION, or a
|
||||
SUGGESTION that has become a FAIL, is damage this flow caused and is in scope for it. Only the
|
||||
improve flow can make that comparison; the create flow has no prior state to compare against.
|
||||
|
||||
Then return to `SKILL.md` Step 4.
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
source_keys:
|
||||
- agentskills-best-practices
|
||||
- agentskills-optimizing-descriptions
|
||||
---
|
||||
|
||||
# Retrofitting a skill to the ADR-0020 contract
|
||||
|
||||
Read this when `references/improve.md` Step 4 sends you here: the skill you are editing is over
|
||||
the description or body budget and has to come into contract before any other change can be
|
||||
committed. The gates are hot and carry no baseline file, so a one-line fix to a non-compliant
|
||||
skill is blocked until this is done.
|
||||
|
||||
Measure first. Do not guess which gate fired: run `/skill-audit` on the directory and read its
|
||||
`### Structure` dimension, which reports the description characters and the **body-only** word
|
||||
count separately from the whole-file spec backstop. Retrofit against the number that actually
|
||||
fired — a skill can sit a thousand words inside the whole-file backstop while failing the body
|
||||
budget.
|
||||
|
||||
**Validate in place.** Audit the skill's real directory inside its package. Never audit a copy in a
|
||||
scratch directory, and never move a skill out to work on it: the boundary-target universe is built
|
||||
by walking up *from the file being checked*, so a copy with no authoring root above it resolves
|
||||
against nothing and the check declines rather than running —
|
||||
|
||||
```text
|
||||
INFO boundary-target resolution DID NOT RUN — no skill universe could be determined for
|
||||
this path ... Unchecked target(s): totally-fake-target
|
||||
```
|
||||
|
||||
The run still exits 0, so that line reads as a pass and is not one. Treat `DID NOT RUN` as **not
|
||||
checked**, always. A retrofit signed off on a scratch copy carries an unverified boundary target
|
||||
into the corpus, which is precisely the failure this gate exists to catch.
|
||||
|
||||
## Cut in this order
|
||||
|
||||
Work the list top down and stop as soon as the gate clears. The order is by ratio of tokens
|
||||
removed to behaviour lost — inverting it is how a retrofit ends up deleting the one instruction
|
||||
the skill existed to carry.
|
||||
|
||||
1. **Gotchas that paraphrase a step in the body below.** Zero information, and already a FAIL on
|
||||
its own. Delete the Gotcha, keep the step.
|
||||
2. **Spec restatements** — text that repeats a published specification, a tool's `--help`, or a
|
||||
ceiling the validator already enforces. The agent gets this right without it. Delete, or move
|
||||
the table to `references/` if a flow genuinely needs to look it up.
|
||||
3. **Capability enumeration** — in a description, the feature list after the trigger clause; in a
|
||||
body, the paragraph that recites what the skill can do. One capability clause survives in the
|
||||
description; the rest belongs in `README.md`.
|
||||
4. **Per-flow prose** — anything only one branch of the procedure ever reaches. This is the
|
||||
largest single win in most bodies, and it is a *move*, not a delete: each flow gets its own
|
||||
self-contained `references/` file, wired from a dispatch table.
|
||||
|
||||
If the body is still over after all four, the skill is doing two jobs. Split it, and say so
|
||||
rather than compressing prose until it stops being readable.
|
||||
|
||||
## What "mutually exclusive flows" means
|
||||
|
||||
Two or more flows that a single invocation cannot both take. The three-way test, copied verbatim
|
||||
from the body-discipline rubric `/skill-audit` judges against — nothing to load, it is quoted in
|
||||
full here:
|
||||
|
||||
> separate subcommands, separate input types, separate lifecycle stages
|
||||
|
||||
Any one of the three is enough. Two flows that differ only in a parameter value are one flow.
|
||||
At two or more mutually exclusive flows a dispatch table is **mandatory** regardless of word
|
||||
count, because every invocation otherwise pays for every branch it did not take.
|
||||
|
||||
## Reference-file conventions
|
||||
|
||||
The create flow owns these rules, and this flow is forbidden from reading `references/create.md`,
|
||||
so what a retrofit needs is restated here:
|
||||
|
||||
- **One topic per file.** A file mixing two concerns gets loaded for one of them and spends the
|
||||
caller's context on the other.
|
||||
- **Kebab-case filenames**, named after the topic rather than the flow that reads it —
|
||||
`body-discipline.md`, not `step-3.md`.
|
||||
- **Wire every file with the literal conditional form** ``If <condition>, read
|
||||
`references/<file>.md` ``. A generic pointer ("see `references/` for details") is a Vale error.
|
||||
- **Two hops from `SKILL.md`, never three.** A flow file may route on to a shared contract file;
|
||||
a file reachable only through two intermediates is rarely loaded when it is needed.
|
||||
- **`source_keys` frontmatter.** If the content you are moving drew on a research source, the new
|
||||
file needs top-level `source_keys:` frontmatter listing those slugs, and every slug must already
|
||||
exist as an `## <slug>` heading in `references/sources.md`. Moving sourced content out of
|
||||
`SKILL.md` without carrying its slugs across breaks the provenance chain, and `/skill-audit`
|
||||
reports the new file as an INFO with no `source_keys`.
|
||||
|
||||
## Collateral is mandatory, not optional
|
||||
|
||||
Moving content out of a `SKILL.md` leaves three files describing a structure that no longer
|
||||
exists. `/skill-audit`'s provenance check exits clean on all three of these, so nothing catches
|
||||
them for you. After every retrofit that adds, removes or renames a file:
|
||||
|
||||
- [ ] **`README.md` file table** — a row for every new `references/` file, and no row left for a
|
||||
file that is gone. Say what triggers the load, not just what the file contains.
|
||||
- [ ] **`references/README.md`**, where the skill has one — same update, same reason.
|
||||
- [ ] **`references/sources.md` → `Contributing files`** — add the new file to every slug whose
|
||||
content moved into it, and remove any file the retrofit deleted. This is the one that gets
|
||||
missed: `sources.md` keeps citing sections of `SKILL.md` that no longer exist, the
|
||||
provenance check still exits 0, and the stale claim survives review.
|
||||
- [ ] Re-run `/skill-audit` and confirm its `### Provenance` dimension does not report the new
|
||||
file as missing `source_keys`.
|
||||
|
||||
## Worked example — a description retrofit
|
||||
|
||||
`gitea-issues` before, 827 characters, the single most common shape in the corpus:
|
||||
|
||||
```text
|
||||
Use when reading or writing Gitea issues: listing repo issues, getting a single issue's details/
|
||||
comments/labels, creating an issue, updating its state, adding or editing comments, applying
|
||||
labels via issue_write, or searching issues/PRs across repositories. Triggers on "create an
|
||||
issue", "what issues are open", "get issue #N", "close issue #N", "comment on issue #N", "search
|
||||
issues for X" — even when the user doesn't say "Gitea" explicitly. Composes gitea-labels-
|
||||
milestones for all label inference/resolution and milestone lookup — do not use this skill to
|
||||
manage label or milestone definitions themselves (create/edit/delete a label, create/close a
|
||||
milestone), that's gitea-labels-milestones directly. Do not use for pull requests (use gitea-prs)
|
||||
or for local git branch/commit work (use gitea-branches or git-branches).
|
||||
```
|
||||
|
||||
After, 240 characters:
|
||||
|
||||
```text
|
||||
Use when reading or writing Gitea issues — list, read, create, comment on, label, close, or
|
||||
search — even when the user does not say "Gitea". Not pull requests -> `gitea-prs`. Not label or
|
||||
milestone definitions -> `gitea-labels-milestones`.
|
||||
```
|
||||
|
||||
What came out, and why:
|
||||
|
||||
| Removed | Why |
|
||||
|---|---|
|
||||
| The second trigger register — `Triggers on "create an issue", "what issues are open", …` | The same triggers restated as quoted user phrasings. Two registers of one trigger list is a FAIL, not a suggestion. |
|
||||
| `applying labels via issue_write` | Implementation detail. The router does not choose a skill by which MCP call it makes. |
|
||||
| `Composes gitea-labels-milestones for all label inference/resolution and milestone lookup` | A composition note. It changes no routing decision and belongs in `README.md`. |
|
||||
| The parenthetical `(create/edit/delete a label, create/close a milestone)` | Capability enumeration inside a boundary clause. The boundary needs the target, not its feature list. |
|
||||
| The `gitea-branches` / `git-branches` boundary | Dropped entirely. Neither was ever going to win an issue request, so the clause defended against nothing — an invented boundary costs characters and buys no routing accuracy. |
|
||||
| `Do not use for pull requests (use gitea-prs)` prose form | Kept, but rewritten as `Not pull requests -> \`gitea-prs\`.` The rewrite buys characters and one uniform shape for the router — not safety. Both forms are parsed **and** target-checked, so a typo in the prose form dangles exactly as an arrow typo does. |
|
||||
|
||||
What stayed: one trigger clause, one capability clause, the indirect trigger (genuinely warranted
|
||||
here — people say "create an issue", not "create a Gitea issue"), and the boundary clauses.
|
||||
|
||||
## Two rules the gates enforce but the prose does not spell out
|
||||
|
||||
**Boundary clauses may be plural.** Write one per genuine near-miss — the example above carries
|
||||
two, because two different skills could each steal activations. "A boundary clause" in the
|
||||
contract means *at least one*, not *exactly one*. What is banned is a boundary clause invented for
|
||||
a skill that was never going to compete, not a second real one.
|
||||
|
||||
**Never let a hyphenated routing target wrap across lines in a folded `>` scalar.** YAML folding
|
||||
replaces the newline with a space, so `gitea-labels-` at the end of one line and `milestones` at
|
||||
the start of the next fold into `gitea-labels- milestones`. `validate.sh` then reads the target as
|
||||
`gitea-labels`, finds no such skill, and reports a dangling boundary target — the live finding on
|
||||
`gitea-issues` today. Reflow the line so the whole name sits on one of them. The same applies to
|
||||
any backticked skill or agent name in a description.
|
||||
@@ -34,7 +34,7 @@ source_keys:
|
||||
- **URL:** https://agentskills.io/skill-creation/best-practices.md
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
|
||||
- **Description:** Best practices for skill creators — starting from real expertise, spending context wisely, calibrating control, instruction patterns (gotchas, templates, checklists, validation loops)
|
||||
- **Contributing files:** SKILL.md, references/create.md, references/improve.md, references/contract.md
|
||||
- **Contributing files:** SKILL.md, references/create.md, references/improve.md, references/contract.md, references/retrofit.md
|
||||
- **Status:** `extracted`
|
||||
|
||||
## agentskills-optimizing-descriptions
|
||||
@@ -42,7 +42,7 @@ source_keys:
|
||||
- **URL:** https://agentskills.io/skill-creation/optimizing-descriptions.md
|
||||
- **Research doc:** plugins/kyberforge/docs/research/docs/agentskillsio/sources.md
|
||||
- **Description:** How to systematically test and improve skill descriptions for triggering accuracy — eval queries, trigger rate testing, train/validation splits, optimization loop
|
||||
- **Contributing files:** SKILL.md, references/improve.md, references/contract.md
|
||||
- **Contributing files:** SKILL.md, references/improve.md, references/contract.md, references/retrofit.md
|
||||
- **Status:** `extracted`
|
||||
|
||||
## agentskills-evaluating-skills
|
||||
|
||||
@@ -127,8 +127,8 @@ for ini in "$SKILL_INI" "$AGENT_INI"; do
|
||||
err "$rel_ini has no section whose BasedOnStyles names Kyberforge — every rule the audit prefilters on lives in that style"
|
||||
fi
|
||||
# Per-rule overrides are the third way to retire a rule without touching a
|
||||
# style file or a glob. CONTEXT.md's "Vale audit prefilter" entry: "Every rule
|
||||
# is `level: error` and every alert is a FAIL — no ignorable tier". Vale's exit
|
||||
# style file or a glob. Per ADR-0013, every rule is `level: error` and every
|
||||
# alert is a FAIL — there is no ignorable tier. Vale's exit
|
||||
# code keys on `error` alerts alone, so any override that leaves a rule at
|
||||
# anything other than `error` still lints the file, still exits 0, and still
|
||||
# shows `Passed` in pre-commit. The glob probe below cannot backstop this: it
|
||||
@@ -175,9 +175,10 @@ for ini in "$SKILL_INI" "$AGENT_INI"; do
|
||||
fi
|
||||
done
|
||||
|
||||
# KyberforgeCopilot is agent-audit's alone — CONTEXT.md describes it as "scoped
|
||||
# only to `.agent.md` files for the Copilot-only 'Use proactively has no effect'
|
||||
# check". The loop above deliberately asserts only `Kyberforge`, since
|
||||
# KyberforgeCopilot is agent-audit's alone — ADR-0013 scopes it to `.agent.md`
|
||||
# files only, for the Copilot-only 'Use proactively has no effect' check, and
|
||||
# records that it must not be extended to `.md` files. The loop above
|
||||
# deliberately asserts only `Kyberforge`, since
|
||||
# skill-audit's copy legitimately has no Copilot style, so dropping
|
||||
# `, KyberforgeCopilot` from agent-audit's `[**/*.agent.md]` section unloaded the
|
||||
# whole style silently: no glob broke, the styles/ diff above stayed clean (the
|
||||
@@ -353,10 +354,10 @@ while IFS='|' read -r skill rel scope; do
|
||||
# `.pre-commit-config.yaml`'s regex correctly no longer matches it and that's
|
||||
# not drift. `demo.agent.md` is the real, current shape and is `shared`.
|
||||
#
|
||||
# The two `.claude/`-prefixed probes carry the location-independence CONTEXT.md
|
||||
# asserts: "A `SKILL.md` outside `plugins/` (e.g. project-scope
|
||||
# The two `.claude/`-prefixed probes carry the location-independence property: a
|
||||
# `SKILL.md` outside `plugins/` (e.g. project-scope
|
||||
# `.claude/skills/foo/SKILL.md`) still matches `[**/SKILL.md]` and gets linted
|
||||
# normally — the globs constrain filename shape, not location." Every other
|
||||
# normally — the globs constrain filename shape, not location. Every other
|
||||
# probe here starts with `plugins/`, so narrowing a glob to a `plugins/`-shaped
|
||||
# path (`[**/SKILL.md]` -> `[**/.apm/skills/*/SKILL.md]`) left all of them
|
||||
# matching while the project-scope shape started linting as `0 errors ... in 0
|
||||
|
||||
+903
-191
File diff suppressed because it is too large.
Load diff
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
# that same file at .claude-plugin/marketplace.json directly, but also has a legacy
|
||||
# convention path at .github/plugin/marketplace.json (see
|
||||
# plugins/kyberforge/docs/research/docs/github-copilot-plugins/marketplace.md) -- and
|
||||
# CONTEXT.md documents that path as a mirror of the Claude output, not a separate apm
|
||||
# that path is a mirror of the Claude output, not a separate apm
|
||||
# output profile (apm only ships "claude" and "codex" mappers; codex writes a
|
||||
# differently-shaped file to .agents/plugins/marketplace.json, not this path). This
|
||||
# script keeps that legacy mirror byte-identical to .claude-plugin/marketplace.json
|
||||
@@ -60,7 +60,19 @@ fi
|
||||
if [[ "$CHECK" -eq 1 ]]; then
|
||||
if [[ ! -f "$DST" ]] || ! diff -q "$SRC" "$DST" >/dev/null 2>&1; then
|
||||
echo "DRIFT $DST: out of sync with .claude-plugin/marketplace.json" >&2
|
||||
echo "Fix: bash scripts/sync-marketplace-mirror.sh" >&2
|
||||
# The runnable command gets a line to ITSELF, and the rationale gets its own
|
||||
# echo. It was one line -- `Fix: bash scripts/sync-marketplace-mirror.sh --
|
||||
# apm ships no output profile...` -- which put the prose after `--`, the
|
||||
# POSIX end-of-options marker, so copy-pasting the Fix line ran this script
|
||||
# with ~24 stray argv entries: `${1:-}` was `--` (so CHECK stayed 0 and no
|
||||
# shift happened), `[[ $# -eq 0 ]]` failed, and the tool meant to fix the
|
||||
# drift answered with its own usage error and exit 1. The backticks around
|
||||
# `apm pack` made it worse: the paste also command-substituted a real
|
||||
# `apm pack` run before the script was even reached. Hence plain quotes
|
||||
# below too. Keep the command alone on its line.
|
||||
echo "Fix: run, from the repository root:" >&2
|
||||
echo " bash scripts/sync-marketplace-mirror.sh" >&2
|
||||
echo "Note: apm ships no output profile targeting this path, so 'apm pack' does not refresh it. Expecting it to is exactly the drift this script and its pre-push hook exist to prevent." >&2
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
|
||||
+24
-6
@@ -13,11 +13,13 @@
|
||||
# dev binary, the other 15 suites still tell you something, and turning that
|
||||
# into a red run would just train people to ignore red.
|
||||
# * as a GATE (the run-tests pre-push hook): a skip is a SETUP ERROR, not a
|
||||
# legitimate state. AGENTS.md documents vale, apm and jq as required pre-push
|
||||
# dependencies, so a suite that cannot run on the machine doing the pushing
|
||||
# means the machine is misconfigured -- and pre-commit prints NOTHING for a
|
||||
# passing hook, so the skip list below is swallowed entirely. On a vale-less
|
||||
# PATH that silently shipped a green gate having verified 15 of 17 suites.
|
||||
# legitimate state. README.md's Prerequisites table documents vale, apm and
|
||||
# python3/PyYAML -- the dependencies these suites actually guard on -- as
|
||||
# required pre-push, so a suite that cannot run on the machine doing the
|
||||
# pushing means the machine is misconfigured -- and
|
||||
# pre-commit prints NOTHING for a passing hook, so the skip list below is
|
||||
# swallowed entirely. On a vale-less PATH that once silently shipped a
|
||||
# green gate having verified 15 of the 17 suites that existed then.
|
||||
# Exactly the vacuous-pass class the rest of this file exists to close.
|
||||
#
|
||||
# Deliberately its own switch, NOT folded into
|
||||
@@ -36,6 +38,22 @@ STRICT=false
|
||||
if [[ "${RUN_TESTS_STRICT:-}" == "1" ]]; then
|
||||
STRICT=true
|
||||
fi
|
||||
# Latched, then REMOVED from the environment. The value has done its only job by
|
||||
# this line -- it is now held in the STRICT shell local -- and leaving it exported
|
||||
# makes strictness leak down the whole process tree: every test-*.sh dispatched
|
||||
# through batch_run below inherits it, and any of them that itself invokes
|
||||
# run-tests.sh (tests/test-run-tests.sh drives a copy of this script over fixture
|
||||
# trees) silently turns a deliberately non-strict fixture strict.
|
||||
#
|
||||
# That is not symmetric with `--strict`, which never leaked: the flag only ever
|
||||
# sets the shell local above, so `bash tests/run-tests.sh --strict` (the spelling
|
||||
# the run-tests pre-push hook uses) always gave children a clean environment. Only
|
||||
# the env-var spelling leaked, and it broke exactly two assertions in
|
||||
# tests/test-run-tests.sh -- its cases 10c and 10g. Unsetting here makes the two
|
||||
# documented invocations equivalent in what a CHILD sees, not just in the parent's
|
||||
# verdict, so no future suite has to defend itself the way test-run-tests.sh's
|
||||
# run_fake() does with `env -u`.
|
||||
unset RUN_TESTS_STRICT
|
||||
# A loop rather than the `[[ "${1:-}" == --bats-only ]]` test this used to be, so
|
||||
# the two flags compose and an unknown flag is rejected instead of ignored. A
|
||||
# silently-ignored `--strict` is the one typo that would turn the gate back off.
|
||||
@@ -242,7 +260,7 @@ fi
|
||||
# here (not just referenced) because this block goes to stderr and is what a
|
||||
# pre-push reader actually gets handed.
|
||||
if [[ "$STRICT" == true && ${#SKIPPED[@]} -gt 0 ]]; then
|
||||
echo "Error: --strict and ${#SKIPPED[@]} suite(s) skipped. Run as a gate, a skip is a SETUP ERROR on this machine, not a legitimate state: AGENTS.md documents vale, apm and jq as required pre-push dependencies, so every suite is expected to be runnable here. Install what each suite names below and re-run; do not skip the hook." >&2
|
||||
echo "Error: --strict and ${#SKIPPED[@]} suite(s) skipped. Run as a gate, a skip is a SETUP ERROR on this machine, not a legitimate state: README.md's Prerequisites table documents vale, apm and python3/PyYAML — what these suites guard on — as required pre-push dependencies, so every suite is expected to be runnable here. Install what each suite names below and re-run; do not skip the hook." >&2
|
||||
sidx=0
|
||||
for s in ${SKIPPED[@]+"${SKIPPED[@]}"}; do
|
||||
echo " $s" >&2
|
||||
|
||||
Executable
+476
@@ -0,0 +1,476 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression test for the ADR-0020 body-shape checks and, just as importantly,
|
||||
# for the false-positive fixes each of them needed. Every check here was
|
||||
# completely untested.
|
||||
#
|
||||
# * Gotchas section over 5 entries — SUGGESTION
|
||||
# * Gotchas section over 25% of the body — SUGGESTION
|
||||
# * a references/<file>.md named but absent — ERROR (a broken pointer is not a
|
||||
# style opinion)
|
||||
# * description with no boundary clause — SUGGESTION
|
||||
#
|
||||
# The false-positive half is not optional extra coverage. Each of these checks
|
||||
# scans prose, and the first naive version of each one fired on ordinary writing:
|
||||
# a ```-fenced EXAMPLE of a Gotchas section became the section itself, indented
|
||||
# child bullets were counted as top-level entries, `## Gotcha handling` was read
|
||||
# as the Gotchas section, and a documented-then-removed references/ file became a
|
||||
# hard ERROR. The skills most likely to carry such an example are skill-author and
|
||||
# skill-audit — the two that DOCUMENT these conventions — so a gate that fires on
|
||||
# them is a gate nobody can turn on.
|
||||
#
|
||||
# Every case is a matched pair: the check fires just over its boundary, and stays
|
||||
# silent just under it (or on the shape it must not match). A test asserting only
|
||||
# that a bad file fails proves nothing about a check that fires on everything.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
HOOK="$REPO_ROOT/scripts/skill-size-check.sh"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
TMPDIR_T="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMPDIR_T"' EXIT
|
||||
|
||||
# A description with a boundary clause and no routing target — the neutral
|
||||
# default, so a fixture about Gotchas or references does not also trip the
|
||||
# missing-boundary-clause SUGGESTION and stop isolating what it names.
|
||||
CLEAN_DESC="Use when doing the thing. Do not use for anything else."
|
||||
|
||||
# make_skill <name> <desc> — SKILL.md with the body read from stdin. Echoes the
|
||||
# path. Each skill gets its own directory so references/ fixtures are isolated.
|
||||
make_skill() {
|
||||
local name="$1" desc="$2" dir
|
||||
dir="$TMPDIR_T/$name"
|
||||
mkdir -p "$dir"
|
||||
{
|
||||
echo "---"
|
||||
echo "name: $name"
|
||||
echo "description: $desc"
|
||||
echo "---"
|
||||
cat
|
||||
} > "$dir/SKILL.md"
|
||||
echo "$dir/SKILL.md"
|
||||
}
|
||||
|
||||
# expect <label> <file> <expect: silent|suggests|errors> [needle] [absent-needle]
|
||||
#
|
||||
# `silent` is the strict one: exit 0 AND completely empty output. It is what
|
||||
# makes every "must not fire" case below real — a check that fired with some
|
||||
# other wording would still be caught.
|
||||
expect() {
|
||||
local label="$1" file="$2" mode="$3" needle="${4:-}" absent="${5:-}" out status=0
|
||||
set +e
|
||||
out="$(bash "$HOOK" "$file" 2>&1)"
|
||||
status=$?
|
||||
set -e
|
||||
case "$mode" in
|
||||
silent)
|
||||
if [[ $status -eq 0 && -z "$out" ]]; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label (exit $status, output: ${out:-<empty>})"
|
||||
fi
|
||||
;;
|
||||
suggests)
|
||||
if [[ $status -ne 0 ]]; then
|
||||
fail "$label — a SUGGESTION must never change the exit code (exit $status, output: $out)"
|
||||
elif [[ "$out" != *"SUGGESTION"* || "$out" != *"$needle"* ]]; then
|
||||
fail "$label (exit $status, output: ${out:-<empty>})"
|
||||
elif [[ -n "$absent" && "$out" == *"$absent"* ]]; then
|
||||
fail "$label — output also contained '$absent', which must not fire here: $out"
|
||||
else
|
||||
pass "$label"
|
||||
fi
|
||||
;;
|
||||
errors)
|
||||
if [[ $status -eq 0 ]]; then
|
||||
fail "$label — expected a hard ERROR, got exit 0 (output: ${out:-<empty>})"
|
||||
elif [[ "$out" != *"$needle"* ]]; then
|
||||
fail "$label (exit $status, output: ${out:-<empty>})"
|
||||
else
|
||||
pass "$label"
|
||||
fi
|
||||
;;
|
||||
quiet-about)
|
||||
# Exit 0 and the named text absent, but other output permitted. Used where
|
||||
# a second, unrelated finding legitimately fires.
|
||||
if [[ $status -ne 0 ]]; then
|
||||
fail "$label (exit $status, output: $out)"
|
||||
elif [[ "$out" == *"$needle"* ]]; then
|
||||
fail "$label — '$needle' fired when it must not: $out"
|
||||
else
|
||||
pass "$label"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
filler() { python3 -c "print(' '.join(['word'] * $1))"; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Gotchas: entry count (guideline 5)
|
||||
# ---------------------------------------------------------------------------
|
||||
# The filler after the section keeps the 25% fraction check well clear, so these
|
||||
# two cases isolate the ENTRY count. Without it a six-entry section in a short
|
||||
# body would fire both and the pair would not distinguish them.
|
||||
echo ""
|
||||
echo "--- Gotchas entry count: 5 is fine, 6 is a SUGGESTION ---"
|
||||
F_FIVE="$(make_skill gotchas-five "$CLEAN_DESC" <<EOF
|
||||
|
||||
## Gotchas
|
||||
|
||||
- first trap here
|
||||
- second trap here
|
||||
- third trap here
|
||||
- fourth trap here
|
||||
- fifth trap here
|
||||
|
||||
## Notes
|
||||
|
||||
$(filler 200)
|
||||
EOF
|
||||
)"
|
||||
expect "a Gotchas section with exactly 5 entries is silent" "$F_FIVE" silent
|
||||
|
||||
F_SIX="$(make_skill gotchas-six "$CLEAN_DESC" <<EOF
|
||||
|
||||
## Gotchas
|
||||
|
||||
- first trap here
|
||||
- second trap here
|
||||
- third trap here
|
||||
- fourth trap here
|
||||
- fifth trap here
|
||||
- sixth trap here
|
||||
|
||||
## Notes
|
||||
|
||||
$(filler 200)
|
||||
EOF
|
||||
)"
|
||||
expect "a Gotchas section with 6 entries raises a SUGGESTION and still exits 0" \
|
||||
"$F_SIX" suggests "Gotchas section has 6 entries" "over the 25% guideline"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Gotchas: share of the body (guideline 25%)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exact boundary arithmetic, not an approximation. The section is prose (no list
|
||||
# items) so the entry check cannot fire and confuse the result; body words are
|
||||
# then section + filler + the two two-word headings. At a body of 100 words a
|
||||
# 25-word section is exactly the guideline (inclusive — `>` is the comparison, so
|
||||
# it passes) and a 26-word section is one word past it.
|
||||
echo ""
|
||||
echo "--- Gotchas share of body: exactly 25% is fine, 26% is a SUGGESTION ---"
|
||||
F_AT="$(make_skill gotchas-at-fraction "$CLEAN_DESC" <<EOF
|
||||
|
||||
## Gotchas
|
||||
|
||||
$(filler 25)
|
||||
|
||||
## Notes
|
||||
|
||||
$(filler 71)
|
||||
EOF
|
||||
)"
|
||||
expect "a Gotchas section at exactly 25% of the body is silent" "$F_AT" silent
|
||||
|
||||
F_OVER="$(make_skill gotchas-over-fraction "$CLEAN_DESC" <<EOF
|
||||
|
||||
## Gotchas
|
||||
|
||||
$(filler 26)
|
||||
|
||||
## Notes
|
||||
|
||||
$(filler 70)
|
||||
EOF
|
||||
)"
|
||||
expect "a Gotchas section at 26% of the body raises a SUGGESTION" \
|
||||
"$F_OVER" suggests "Gotchas section is 26 of 100 body words (26%)" "entries"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Gotchas: false positives
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- a ## Gotchas heading inside a fenced block is not the Gotchas section ---"
|
||||
# skill-author and skill-audit both document this convention by showing it. If a
|
||||
# fenced example counted, the two skills that define the rule would be the two
|
||||
# most likely to fail it.
|
||||
F_FENCED_HEADING="$(make_skill gotchas-fenced-heading "$CLEAN_DESC" <<EOF
|
||||
|
||||
## How to write one
|
||||
|
||||
\`\`\`markdown
|
||||
## Gotchas
|
||||
|
||||
- example one
|
||||
- example two
|
||||
- example three
|
||||
- example four
|
||||
- example five
|
||||
- example six
|
||||
- example seven
|
||||
\`\`\`
|
||||
|
||||
$(filler 200)
|
||||
EOF
|
||||
)"
|
||||
expect "a fenced ## Gotchas heading is not read as the section" \
|
||||
"$F_FENCED_HEADING" quiet-about "Gotchas section"
|
||||
|
||||
echo ""
|
||||
echo "--- list items inside a fenced block do not count as Gotchas entries ---"
|
||||
F_FENCED_ENTRIES="$(make_skill gotchas-fenced-entries "$CLEAN_DESC" <<EOF
|
||||
|
||||
## Gotchas
|
||||
|
||||
- a real trap
|
||||
- another real trap
|
||||
|
||||
Shown as an example of what NOT to write:
|
||||
|
||||
\`\`\`markdown
|
||||
- fake one
|
||||
- fake two
|
||||
- fake three
|
||||
- fake four
|
||||
- fake five
|
||||
- fake six
|
||||
- fake seven
|
||||
- fake eight
|
||||
\`\`\`
|
||||
|
||||
## Notes
|
||||
|
||||
$(filler 300)
|
||||
EOF
|
||||
)"
|
||||
expect "eight fenced bullets plus two real ones counts as two entries, not ten" \
|
||||
"$F_FENCED_ENTRIES" quiet-about "Gotchas section has"
|
||||
|
||||
echo ""
|
||||
echo "--- the heading must END in gotcha(s): '## Gotcha handling' is not the section ---"
|
||||
# `## Gotcha handling` and `## Why gotchas matter` are prose sections. Treating
|
||||
# one as the Gotchas section measures a span that was never a gotcha list.
|
||||
F_HANDLING="$(make_skill gotcha-handling "$CLEAN_DESC" <<EOF
|
||||
|
||||
## Gotcha handling
|
||||
|
||||
- item one
|
||||
- item two
|
||||
- item three
|
||||
- item four
|
||||
- item five
|
||||
- item six
|
||||
- item seven
|
||||
|
||||
## Notes
|
||||
|
||||
$(filler 200)
|
||||
EOF
|
||||
)"
|
||||
expect "'## Gotcha handling' is not matched as the Gotchas section" \
|
||||
"$F_HANDLING" quiet-about "Gotchas section"
|
||||
|
||||
# The control for the three cases above. Without it, "no Gotchas finding" could
|
||||
# equally mean the whole check is dead, and all three would still be green.
|
||||
F_CONTROL="$(make_skill gotchas-control "$CLEAN_DESC" <<EOF
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- item one
|
||||
- item two
|
||||
- item three
|
||||
- item four
|
||||
- item five
|
||||
- item six
|
||||
- item seven
|
||||
|
||||
## Notes
|
||||
|
||||
$(filler 200)
|
||||
EOF
|
||||
)"
|
||||
expect "control: a real '## Common gotchas' heading with 7 entries IS matched" \
|
||||
"$F_CONTROL" suggests "Gotchas section has 7 entries"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# references/<file>.md pointers
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- a references/ pointer that is not on disk is a hard ERROR ---"
|
||||
F_REF_MISSING="$(make_skill ref-missing "$CLEAN_DESC" <<EOF
|
||||
|
||||
If the caller needs the long form, read references/nowhere.md first.
|
||||
EOF
|
||||
)"
|
||||
expect "a body pointing at an absent references/nowhere.md ERRORs" \
|
||||
"$F_REF_MISSING" errors "points at references/nowhere.md"
|
||||
|
||||
F_REF_PRESENT="$(make_skill ref-present "$CLEAN_DESC" <<EOF
|
||||
|
||||
If the caller needs the long form, read references/here.md first.
|
||||
EOF
|
||||
)"
|
||||
mkdir -p "$TMPDIR_T/ref-present/references"
|
||||
echo "content" > "$TMPDIR_T/ref-present/references/here.md"
|
||||
expect "the same pointer is silent once the file exists" "$F_REF_PRESENT" silent
|
||||
|
||||
echo ""
|
||||
echo "--- a references/ pointer inside a fenced block is not a dispatch entry ---"
|
||||
F_REF_FENCED="$(make_skill ref-fenced "$CLEAN_DESC" <<EOF
|
||||
|
||||
Dispatch tables look like this:
|
||||
|
||||
\`\`\`markdown
|
||||
If X, read references/example-file.md.
|
||||
\`\`\`
|
||||
EOF
|
||||
)"
|
||||
expect "a fenced references/example-file.md does not ERROR" "$F_REF_FENCED" silent
|
||||
|
||||
echo ""
|
||||
echo "--- an UNTERMINATED fence does not blank the rest of the body ---"
|
||||
# The fenced-block exemptions above all rest on mask_fenced(), and an unclosed
|
||||
# fence used to run to EOF: everything after it was blanked, so the ERROR-tier
|
||||
# references/ check and both Gotchas counts silently stopped seeing any of it.
|
||||
# That is the worst shape a masking bug can take — a stray ``` line, which is a
|
||||
# typo an author makes while writing the very examples the masking exists for,
|
||||
# turned the rest of the file invisible and the gate green. Masking may narrow
|
||||
# what a check reads; it may never delete content from every check at once.
|
||||
#
|
||||
# Both suppressed checks are asserted, because they are separate call sites and
|
||||
# a fix that restored only one would leave the other silent.
|
||||
F_FENCE_REF="$(make_skill fence-unclosed-ref "$CLEAN_DESC" <<EOF
|
||||
|
||||
Here is how it is invoked:
|
||||
|
||||
\`\`\`bash
|
||||
some-command --all
|
||||
|
||||
If the caller needs the long form, read references/behind-the-fence.md first.
|
||||
EOF
|
||||
)"
|
||||
expect "an absent references/ pointer after an unclosed fence still ERRORs" \
|
||||
"$F_FENCE_REF" errors "points at references/behind-the-fence.md"
|
||||
|
||||
F_FENCE_GOTCHAS="$(make_skill fence-unclosed-gotchas "$CLEAN_DESC" <<EOF
|
||||
|
||||
Here is how it is invoked:
|
||||
|
||||
\`\`\`bash
|
||||
some-command --all
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- first trap here
|
||||
- second trap here
|
||||
- third trap here
|
||||
- fourth trap here
|
||||
- fifth trap here
|
||||
- sixth trap here
|
||||
- seventh trap here
|
||||
|
||||
## Notes
|
||||
|
||||
$(filler 200)
|
||||
EOF
|
||||
)"
|
||||
expect "a Gotchas section after an unclosed fence is still counted" \
|
||||
"$F_FENCE_GOTCHAS" suggests "Gotchas section has 7 entries"
|
||||
|
||||
# The control. Closing the fence must still mask, or the fix above would have
|
||||
# been "stop masking", which re-breaks every false-positive case in this file.
|
||||
F_FENCE_CLOSED="$(make_skill fence-closed-ref "$CLEAN_DESC" <<EOF
|
||||
|
||||
Here is how it is invoked:
|
||||
|
||||
\`\`\`bash
|
||||
some-command --all
|
||||
\`\`\`
|
||||
|
||||
Dispatch tables look like this:
|
||||
|
||||
\`\`\`markdown
|
||||
If X, read references/behind-the-fence.md.
|
||||
\`\`\`
|
||||
EOF
|
||||
)"
|
||||
expect "control: the same pointer inside a CLOSED fence is still masked" \
|
||||
"$F_FENCE_CLOSED" silent
|
||||
|
||||
echo ""
|
||||
echo "--- a references/ pointer in a same-line removal context is history, not dispatch ---"
|
||||
# Narrow on purpose: a live dispatch table never describes its own target as
|
||||
# removed, so the exemption costs no recall. Each phrasing is checked separately
|
||||
# because they are separate alternatives in one regex, and a typo in any one of
|
||||
# them turns ordinary prose back into a hard ERROR.
|
||||
#
|
||||
# The file names are deliberately NEUTRAL (detail-a.md, not gone-a.md). An
|
||||
# earlier draft of this block named them gone-*.md and every case passed for the
|
||||
# wrong reason: "gone" is itself one of the removal words, so the exemption fired
|
||||
# off the FILENAME and the phrase under test was never exercised. The control
|
||||
# below is what surfaced that — it is the assertion that keeps these five honest.
|
||||
i=0
|
||||
for phrase in \
|
||||
"The old references/detail-a.md was removed in v2." \
|
||||
"references/detail-b.md is no longer part of this skill." \
|
||||
"references/detail-c.md is deprecated and should not be read." \
|
||||
"references/detail-d.md was renamed, so nothing points at it now." \
|
||||
"references/detail-e.md has been superseded by the body itself."; do
|
||||
i=$((i + 1))
|
||||
F_REF_PAST="$(make_skill "ref-past-$i" "$CLEAN_DESC" <<EOF
|
||||
|
||||
$phrase
|
||||
EOF
|
||||
)"
|
||||
expect "removal-context pointer is not an ERROR: \"$phrase\"" "$F_REF_PAST" silent
|
||||
done
|
||||
|
||||
# The control: the SAME sentence shape without a removal word must still ERROR,
|
||||
# or the exemption above has swallowed the check rather than narrowed it.
|
||||
F_REF_LIVE="$(make_skill ref-live "$CLEAN_DESC" <<EOF
|
||||
|
||||
The details live in references/detail-a.md, which the agent should read first.
|
||||
EOF
|
||||
)"
|
||||
expect "control: the same pointer with no removal word still ERRORs" \
|
||||
"$F_REF_LIVE" errors "points at references/detail-a.md"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Missing boundary clause
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- a description with no boundary clause raises a SUGGESTION ---"
|
||||
F_NO_BOUNDARY="$(make_skill no-boundary "Use when the user wants the thing done." <<EOF
|
||||
|
||||
Do the thing.
|
||||
EOF
|
||||
)"
|
||||
expect "a description with no boundary clause raises a SUGGESTION and still exits 0" \
|
||||
"$F_NO_BOUNDARY" suggests "description has no boundary clause"
|
||||
|
||||
# Both accepted shapes, asserted separately: the prose markers and ADR-0020's
|
||||
# compressed arrow form. Dropping either from the detector would leave the other
|
||||
# green.
|
||||
F_PROSE_BOUNDARY="$(make_skill prose-boundary "Use when the user wants the thing done. Do not use for anything else." <<EOF
|
||||
|
||||
Do the thing.
|
||||
EOF
|
||||
)"
|
||||
expect "the prose boundary form satisfies the check" "$F_PROSE_BOUNDARY" silent
|
||||
|
||||
F_ARROW_BOUNDARY="$(make_skill arrow-boundary "Use when the user wants the thing done. Not the other thing -> sibling-skill." <<EOF
|
||||
|
||||
Do the thing.
|
||||
EOF
|
||||
)"
|
||||
expect "ADR-0020's compressed 'Not X -> y' form satisfies the check" \
|
||||
"$F_ARROW_BOUNDARY" quiet-about "no boundary clause"
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
Executable
+331
@@ -0,0 +1,331 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression test for the three STRUCTURAL claims the ADR-0020 gate family makes
|
||||
# about itself. None of them was pinned anywhere before this file, and each one
|
||||
# fails silently — which is the whole reason they need a test rather than a
|
||||
# comment:
|
||||
#
|
||||
# 1. "ONE resolver, embedded VERBATIM in three scripts." The block between the
|
||||
# BEGIN/END markers is copied, not imported, because a cache-installed
|
||||
# plugin's scripts cannot read files outside their own plugin directory.
|
||||
# Nothing but this file asserts the three copies are still identical, and a
|
||||
# one-line edit to a single copy is invisible: every constant-agreement
|
||||
# assertion in tests/test-skill-size-check.sh still passes, because the
|
||||
# CONSTANTS are not what drifted.
|
||||
# 2. Both interpreter preflights, in all three scripts. python3 and PyYAML are
|
||||
# declared HARD dependencies precisely so a missing one cannot turn into a
|
||||
# vacuous pass, and the two are checked separately so the message names the
|
||||
# thing to install rather than the wrong one.
|
||||
# 3. `verbose: true` on the skill-size-check hook. It is the ENTIRE delivery
|
||||
# mechanism for the SUGGESTION tier: pre-commit prints nothing at all for a
|
||||
# passing hook, and a SUGGESTION deliberately does not fail, so dropping
|
||||
# one word from the config silences the tier ADR-0020 depends on while
|
||||
# every test and every hook still reports green.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
HOOK="$REPO_ROOT/scripts/skill-size-check.sh"
|
||||
SKILL_VALIDATE="$REPO_ROOT/plugins/kyberforge/.apm/skills/skill-audit/scripts/validate.sh"
|
||||
AGENT_VALIDATE="$REPO_ROOT/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate.sh"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
TMPDIR_T="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMPDIR_T"' EXIT
|
||||
|
||||
BEGIN_MARKER='# ===== BEGIN ADR-0020 SHARED BOUNDARY RESOLVER ====='
|
||||
END_MARKER='# ===== END ADR-0020 SHARED BOUNDARY RESOLVER ====='
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. The shared resolver block is byte-identical in all three scripts
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- the ADR-0020 shared resolver block is byte-identical in all three scripts ---"
|
||||
|
||||
# Marker discipline first. An unbalanced or duplicated marker pair makes the
|
||||
# extraction below silently measure the wrong span — a sed range that never
|
||||
# closes swallows the rest of the file, and one that opens twice concatenates
|
||||
# two spans. Both would still compare "equal" if all three were mangled the
|
||||
# same way, so the shape is asserted before the contents.
|
||||
MARKERS_OK=true
|
||||
for f in "$HOOK" "$SKILL_VALIDATE" "$AGENT_VALIDATE"; do
|
||||
if [[ ! -f "$f" ]]; then
|
||||
fail "script not found: $f"
|
||||
MARKERS_OK=false
|
||||
continue
|
||||
fi
|
||||
b="$(grep -cFx "$BEGIN_MARKER" "$f" || true)"
|
||||
e="$(grep -cFx "$END_MARKER" "$f" || true)"
|
||||
if [[ "$b" == "1" && "$e" == "1" ]]; then
|
||||
pass "${f#"$REPO_ROOT/"} carries exactly one BEGIN and one END marker"
|
||||
else
|
||||
fail "${f#"$REPO_ROOT/"} has $b BEGIN and $e END markers, expected 1 and 1"
|
||||
MARKERS_OK=false
|
||||
fi
|
||||
done
|
||||
|
||||
if ! $MARKERS_OK; then
|
||||
fail "skipping the byte-identity comparison — the marker pairs are not well-formed, so any extraction would measure the wrong span"
|
||||
else
|
||||
HASHES=()
|
||||
LINECOUNTS=()
|
||||
for f in "$HOOK" "$SKILL_VALIDATE" "$AGENT_VALIDATE"; do
|
||||
out="$TMPDIR_T/block-$(echo "$f" | md5sum | cut -c1-8).txt"
|
||||
sed -n "/^${BEGIN_MARKER}\$/,/^${END_MARKER}\$/p" "$f" > "$out"
|
||||
HASHES+=("$(md5sum < "$out" | cut -d' ' -f1)")
|
||||
LINECOUNTS+=("$(wc -l < "$out" | tr -d ' ')")
|
||||
done
|
||||
if [[ "${HASHES[0]}" == "${HASHES[1]}" && "${HASHES[1]}" == "${HASHES[2]}" ]]; then
|
||||
pass "all three copies hash to ${HASHES[0]} (${LINECOUNTS[0]} lines) — agreement by construction, not by coincidence"
|
||||
else
|
||||
fail "the shared resolver has DRIFTED: skill-size-check=${HASHES[0]} (${LINECOUNTS[0]} lines), skill-audit=${HASHES[1]} (${LINECOUNTS[1]} lines), agent-audit=${HASHES[2]} (${LINECOUNTS[2]} lines). Edit one copy, then paste it over the other two."
|
||||
fi
|
||||
# A block that has been emptied out would hash equal in all three and pass the
|
||||
# comparison above while enforcing nothing. The resolver is ~570 lines; 100 is
|
||||
# a floor low enough never to need maintenance and high enough that a gutted
|
||||
# block cannot sneak past.
|
||||
if [[ "${LINECOUNTS[0]}" -gt 100 ]]; then
|
||||
pass "the extracted block is ${LINECOUNTS[0]} lines — the comparison is over real content, not an empty span"
|
||||
else
|
||||
fail "the extracted shared block is only ${LINECOUNTS[0]} lines — three identical empty spans would compare equal and assert nothing"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Both interpreter preflights, in all three scripts
|
||||
# ---------------------------------------------------------------------------
|
||||
# The two are checked separately on purpose: `python3 -c 'import yaml'` fails
|
||||
# identically whether python3 is missing or PyYAML is, and naming the wrong one
|
||||
# sends the reader to install the wrong thing.
|
||||
REAL_PYTHON="$(command -v python3)"
|
||||
# Absolute path, deliberately. The no-python3 fixture below replaces PATH
|
||||
# wholesale, so a bare `bash` (or `/usr/bin/env bash`) would be resolved against
|
||||
# that stripped PATH and die with "No such file or directory" before the script
|
||||
# under test ever starts -- a 127 that looks like the preflight firing.
|
||||
BASH_BIN="$(command -v bash)"
|
||||
|
||||
# A PATH that genuinely has no python3 on it. Built by symlinking the handful of
|
||||
# binaries the three scripts touch before their own preflight rather than by
|
||||
# hiding python3 from a full PATH, because there is no portable way to subtract
|
||||
# one entry from a directory. `bash` is invoked by absolute path below so the
|
||||
# interpreter itself does not have to be on this PATH.
|
||||
NOPY_BIN="$TMPDIR_T/nopython-bin"
|
||||
mkdir -p "$NOPY_BIN"
|
||||
for b in awk cat cut dirname basename grep sed pwd rm mkdir tr; do
|
||||
src="$(command -v "$b" 2>/dev/null || true)"
|
||||
[[ -n "$src" ]] && ln -sf "$src" "$NOPY_BIN/$b"
|
||||
done
|
||||
|
||||
# A python3 that runs but cannot import yaml. A shim on PATH re-execs the real
|
||||
# interpreter with a PYTHONPATH entry holding a `yaml` module that raises on
|
||||
# import; PYTHONPATH precedes site-packages on sys.path, so it shadows a real
|
||||
# PyYAML install without touching it.
|
||||
SHADOW="$TMPDIR_T/shadow"
|
||||
mkdir -p "$SHADOW"
|
||||
printf 'raise ImportError("PyYAML deliberately unavailable in this fixture")\n' \
|
||||
> "$SHADOW/yaml.py"
|
||||
NOYAML_BIN="$TMPDIR_T/noyaml-bin"
|
||||
mkdir -p "$NOYAML_BIN"
|
||||
cat > "$NOYAML_BIN/python3" <<EOF
|
||||
#!/bin/sh
|
||||
PYTHONPATH="$SHADOW\${PYTHONPATH:+:\$PYTHONPATH}" exec "$REAL_PYTHON" "\$@"
|
||||
EOF
|
||||
chmod +x "$NOYAML_BIN/python3"
|
||||
|
||||
# Sanity-check the two fixtures themselves before trusting any verdict they
|
||||
# produce. A shim that silently still imports yaml would make every PyYAML
|
||||
# assertion below pass for the wrong reason.
|
||||
if PATH="$NOYAML_BIN:$PATH" python3 -c 'import yaml' 2>/dev/null; then
|
||||
fail "the no-PyYAML shim does not actually shadow PyYAML — every PyYAML assertion below would be vacuous"
|
||||
else
|
||||
pass "fixture check: the no-PyYAML shim makes 'import yaml' fail while python3 still runs"
|
||||
fi
|
||||
if PATH="$NOPY_BIN" command -v python3 > /dev/null 2>&1; then
|
||||
fail "the no-python3 PATH still resolves python3 — every python3 assertion below would be vacuous"
|
||||
else
|
||||
pass "fixture check: the no-python3 PATH resolves no python3"
|
||||
fi
|
||||
|
||||
# A minimal, entirely clean subject for each script. The preflight must fire
|
||||
# before any measurement, so the subject's own content is irrelevant — which is
|
||||
# exactly what makes a clean one the right choice: nothing else can produce the
|
||||
# non-zero exit these cases assert.
|
||||
SUBJECT_SKILL_DIR="$TMPDIR_T/subject/my-skill"
|
||||
mkdir -p "$SUBJECT_SKILL_DIR"
|
||||
cat > "$SUBJECT_SKILL_DIR/SKILL.md" <<'EOF'
|
||||
---
|
||||
name: my-skill
|
||||
description: A short valid description. Do not use for anything else.
|
||||
---
|
||||
|
||||
Do the thing.
|
||||
EOF
|
||||
SUBJECT_AGENT_ROOT="$TMPDIR_T/subject-agent"
|
||||
mkdir -p "$SUBJECT_AGENT_ROOT/.apm/agents"
|
||||
cat > "$SUBJECT_AGENT_ROOT/apm.yml" <<'EOF'
|
||||
name: test-package
|
||||
version: 0.1.0
|
||||
type: skill
|
||||
EOF
|
||||
cat > "$SUBJECT_AGENT_ROOT/.apm/agents/my-agent.agent.md" <<'EOF'
|
||||
---
|
||||
name: my-agent
|
||||
description: A short valid description. Do not use for anything else.
|
||||
---
|
||||
|
||||
You are a test agent. When invoked, do the thing.
|
||||
EOF
|
||||
|
||||
# probe_preflight <label> <env-kind: nopython|noyaml> <expect-needle> <cmd...>
|
||||
probe_preflight() {
|
||||
local label="$1" kind="$2" needle="$3"
|
||||
shift 3
|
||||
local out status=0
|
||||
set +e
|
||||
if [[ "$kind" == nopython ]]; then
|
||||
out="$(env -i PATH="$NOPY_BIN" HOME="$HOME" "$BASH_BIN" "$@" 2>&1)"
|
||||
else
|
||||
out="$(env PATH="$NOYAML_BIN:$PATH" "$BASH_BIN" "$@" 2>&1)"
|
||||
fi
|
||||
status=$?
|
||||
set -e
|
||||
if [[ $status -eq 0 ]]; then
|
||||
fail "$label exited 0 — a missing hard dependency became a vacuous pass (output: ${out:-<empty>})"
|
||||
elif [[ "$out" != *"$needle"* ]]; then
|
||||
# The needle is the DIAGNOSTIC ("python3 is required"), not the bare word.
|
||||
# Deleting the preflight entirely would still produce a non-zero exit and a
|
||||
# message mentioning python3 -- bash's own "python3: command not found" --
|
||||
# so a bare-word needle would go green on a script with no preflight at all.
|
||||
fail "$label exited $status but never produced the '$needle' diagnostic (output: ${out:-<empty>})"
|
||||
elif [[ "$kind" == nopython && "$out" == *PyYAML* ]]; then
|
||||
fail "$label reported PyYAML when python3 itself is missing — that sends the reader to install the wrong thing (output: $out)"
|
||||
else
|
||||
pass "$label"
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "--- a PATH with no python3 is a hard failure in all three scripts, naming python3 ---"
|
||||
probe_preflight "scripts/skill-size-check.sh reports missing python3" \
|
||||
nopython "python3 is required" \
|
||||
"$HOOK" "$SUBJECT_SKILL_DIR/SKILL.md"
|
||||
probe_preflight "skill-audit/scripts/validate.sh reports missing python3" \
|
||||
nopython "python3 is required" \
|
||||
"$SKILL_VALIDATE" "$SUBJECT_SKILL_DIR"
|
||||
probe_preflight "agent-audit/scripts/validate.sh reports missing python3" \
|
||||
nopython "python3 is required" \
|
||||
"$AGENT_VALIDATE" "$SUBJECT_AGENT_ROOT/.apm/agents/my-agent.agent.md"
|
||||
|
||||
echo ""
|
||||
echo "--- a python3 that cannot import yaml is a hard failure in all three scripts, naming PyYAML ---"
|
||||
probe_preflight "scripts/skill-size-check.sh reports missing PyYAML" \
|
||||
noyaml "PyYAML is required" \
|
||||
"$HOOK" "$SUBJECT_SKILL_DIR/SKILL.md"
|
||||
probe_preflight "skill-audit/scripts/validate.sh reports missing PyYAML" \
|
||||
noyaml "PyYAML is required" \
|
||||
"$SKILL_VALIDATE" "$SUBJECT_SKILL_DIR"
|
||||
probe_preflight "agent-audit/scripts/validate.sh reports missing PyYAML" \
|
||||
noyaml "PyYAML is required" \
|
||||
"$AGENT_VALIDATE" "$SUBJECT_AGENT_ROOT/.apm/agents/my-agent.agent.md"
|
||||
|
||||
# The control. Without it, "fails when the dependency is missing" is satisfied by
|
||||
# a script that fails unconditionally, and the two cases above would be green on
|
||||
# a gate that never runs at all.
|
||||
echo ""
|
||||
echo "--- control: with both dependencies present the same subjects pass ---"
|
||||
for probe in "$HOOK:$SUBJECT_SKILL_DIR/SKILL.md" \
|
||||
"$SKILL_VALIDATE:$SUBJECT_SKILL_DIR" \
|
||||
"$AGENT_VALIDATE:$SUBJECT_AGENT_ROOT/.apm/agents/my-agent.agent.md"; do
|
||||
script="${probe%%:*}"
|
||||
arg="${probe#*:}"
|
||||
set +e
|
||||
ctl_out="$(bash "$script" "$arg" 2>&1)"
|
||||
ctl_rc=$?
|
||||
set -e
|
||||
if [[ $ctl_rc -eq 0 ]]; then
|
||||
pass "${script#"$REPO_ROOT/"} exits 0 on a clean subject with python3 and PyYAML available"
|
||||
else
|
||||
fail "${script#"$REPO_ROOT/"} failed a clean subject (exit $ctl_rc): $ctl_out"
|
||||
fi
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. verbose: true on the skill-size-check hook, in BOTH manifests
|
||||
# ---------------------------------------------------------------------------
|
||||
# .pre-commit-config.yaml governs this repo; .pre-commit-hooks.yaml is what a
|
||||
# CONSUMER repo gets when it points at this one. Dropping the flag from either
|
||||
# silences the SUGGESTION tier for that audience alone, which is the hardest
|
||||
# version of the defect to notice.
|
||||
echo ""
|
||||
echo "--- the skill-size-check hook declares verbose: true in both manifests ---"
|
||||
VERBOSE_REPORT="$(python3 - "$REPO_ROOT" <<'PY'
|
||||
import os
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
root = sys.argv[1]
|
||||
|
||||
|
||||
def emit(status, msg):
|
||||
print("%s\t%s" % (status, msg))
|
||||
|
||||
|
||||
# Repo config: nested repos[].hooks[].
|
||||
path = os.path.join(root, '.pre-commit-config.yaml')
|
||||
try:
|
||||
with open(path, encoding='utf-8') as fh:
|
||||
cfg = yaml.safe_load(fh) or {}
|
||||
except Exception as exc:
|
||||
emit('FAIL', '.pre-commit-config.yaml did not parse: %s' % exc)
|
||||
cfg = {}
|
||||
found = None
|
||||
for repo in cfg.get('repos') or []:
|
||||
for hook in (repo.get('hooks') or []):
|
||||
if hook.get('id') == 'skill-size-check':
|
||||
found = hook
|
||||
if found is None:
|
||||
emit('FAIL', '.pre-commit-config.yaml declares no hook with id skill-size-check')
|
||||
elif found.get('verbose') is True:
|
||||
emit('PASS', '.pre-commit-config.yaml: skill-size-check is verbose: true')
|
||||
else:
|
||||
emit('FAIL', '.pre-commit-config.yaml: skill-size-check has verbose=%r — '
|
||||
'pre-commit prints nothing for a passing hook, so every '
|
||||
'ADR-0020 SUGGESTION is swallowed' % (found.get('verbose'),))
|
||||
|
||||
# Consumer manifest: a flat list of hooks.
|
||||
path = os.path.join(root, '.pre-commit-hooks.yaml')
|
||||
try:
|
||||
with open(path, encoding='utf-8') as fh:
|
||||
hooks = yaml.safe_load(fh) or []
|
||||
except Exception as exc:
|
||||
emit('FAIL', '.pre-commit-hooks.yaml did not parse: %s' % exc)
|
||||
hooks = []
|
||||
found = None
|
||||
for hook in hooks:
|
||||
if isinstance(hook, dict) and hook.get('id') == 'kyberforge-skill-size-check':
|
||||
found = hook
|
||||
if found is None:
|
||||
emit('FAIL', '.pre-commit-hooks.yaml declares no hook with id kyberforge-skill-size-check')
|
||||
elif found.get('verbose') is True:
|
||||
emit('PASS', '.pre-commit-hooks.yaml: kyberforge-skill-size-check is verbose: true')
|
||||
else:
|
||||
emit('FAIL', '.pre-commit-hooks.yaml: kyberforge-skill-size-check has verbose=%r — '
|
||||
'a consumer repo would never see the SUGGESTION tier'
|
||||
% (found.get('verbose'),))
|
||||
PY
|
||||
)"
|
||||
while IFS=$'\t' read -r status msg; do
|
||||
[[ -n "$status" ]] || continue
|
||||
if [[ "$status" == PASS ]]; then
|
||||
pass "$msg"
|
||||
else
|
||||
fail "$msg"
|
||||
fi
|
||||
done <<< "$VERBOSE_REPORT"
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
Executable
+452
@@ -0,0 +1,452 @@
|
||||
#!/usr/bin/env bash
|
||||
# Differential test: scripts/skill-size-check.sh (the pre-commit hook) and
|
||||
# skill-audit/scripts/validate.sh (the in-skill auditor) must reach the SAME
|
||||
# ADR-0020 verdict on the same file.
|
||||
#
|
||||
# Why this exists as a separate suite. tests/test-skill-size-check.sh already
|
||||
# asserts the two agree on their CONSTANTS, and that assertion is necessary but
|
||||
# demonstrably not sufficient: a previous review found the two scripts disagreeing
|
||||
# on real files while every constant matched perfectly. Constants are one of the
|
||||
# ways two hand-duplicated implementations diverge; comparison operators, message
|
||||
# wording, which value gets measured, and which branch runs first are the others,
|
||||
# and none of them is visible to a constant check.
|
||||
#
|
||||
# The consequence of divergence is specific and bad: skill-audit reports a skill
|
||||
# ready to ship and the commit hook then rejects it, or worse, the reverse. So the
|
||||
# comparison here is over VERDICTS on files, not over source text.
|
||||
#
|
||||
# Scope: every axis the two scripts share. The ADR-0020 ones — description
|
||||
# length and tier, body word count and tier, dangling routing targets, missing
|
||||
# references/ pointers, the two Gotchas suggestions, the missing-boundary-clause
|
||||
# suggestion, a declined resolution, an empty description — plus the two
|
||||
# agentskills.io spec ceilings, MAX_LINES and MAX_WORDS.
|
||||
#
|
||||
# Those last two were EXCLUDED from this comparison until a real divergence
|
||||
# shipped behind the exclusion. The header used to say "the hook checks
|
||||
# whole-file lines and words" as if the auditor did not; it does, from its own
|
||||
# copy of the same two constants, and the two implementations disagreed on
|
||||
# Unicode whitespace for as long as nobody compared them. An axis both scripts
|
||||
# measure is in scope by definition — the only lines still ignored are the ones
|
||||
# a single script owns outright (validate.sh's name/directory agreement, script
|
||||
# executability and 1024-char description backstop).
|
||||
#
|
||||
# Run over the real 39-skill corpus AND over purpose-built fixtures that sit ON
|
||||
# each boundary. The corpus alone is not enough — it happens not to contain a
|
||||
# file at exactly 900 body words, which is precisely where an inclusive/exclusive
|
||||
# comparison mismatch would hide.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
HOOK="$REPO_ROOT/scripts/skill-size-check.sh"
|
||||
SKILL_VALIDATE="$REPO_ROOT/plugins/kyberforge/.apm/skills/skill-audit/scripts/validate.sh"
|
||||
|
||||
TMPDIR_T="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMPDIR_T"' EXIT
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures: one per ADR-0020 axis, placed ON the boundary wherever there is one.
|
||||
# ---------------------------------------------------------------------------
|
||||
# Built inside a synthetic plugin monorepo so boundary-target resolution actually
|
||||
# runs for both scripts (in a bare temp dir both would decline, and "both
|
||||
# declined" is agreement about nothing).
|
||||
FIXTURE_ROOT="$TMPDIR_T/fixtures"
|
||||
FX="$FIXTURE_ROOT/plugins/fixture-plugin/.apm/skills"
|
||||
mkdir -p "$FX/sibling-skill" "$FIXTURE_ROOT/plugins/fixture-plugin/.apm/agents"
|
||||
|
||||
make_fx() {
|
||||
local name="$1" desc="$2" body_words="$3"
|
||||
mkdir -p "$FX/$name"
|
||||
{
|
||||
echo "---"
|
||||
echo "name: $name"
|
||||
echo "description: $desc"
|
||||
echo "---"
|
||||
echo ""
|
||||
python3 -c "print(' '.join(['word'] * $body_words))"
|
||||
} > "$FX/$name/SKILL.md"
|
||||
}
|
||||
desc_of_length() {
|
||||
python3 - "$1" <<'PY'
|
||||
import sys
|
||||
n = int(sys.argv[1])
|
||||
prefix = 'Use when doing the thing. Do not use for anything else. '
|
||||
print(prefix + 'x' * (n - len(prefix)))
|
||||
PY
|
||||
}
|
||||
CLEAN="Use when doing the thing. Do not use for anything else."
|
||||
|
||||
# Description tier boundaries, both sides of both thresholds.
|
||||
make_fx desc-249 "$(desc_of_length 249)" 10
|
||||
make_fx desc-250 "$(desc_of_length 250)" 10
|
||||
make_fx desc-251 "$(desc_of_length 251)" 10
|
||||
make_fx desc-400 "$(desc_of_length 400)" 10
|
||||
make_fx desc-401 "$(desc_of_length 401)" 10
|
||||
# Body tier boundaries, both sides of both thresholds.
|
||||
make_fx body-599 "$CLEAN" 599
|
||||
make_fx body-600 "$CLEAN" 600
|
||||
make_fx body-601 "$CLEAN" 601
|
||||
make_fx body-900 "$CLEAN" 900
|
||||
make_fx body-901 "$CLEAN" 901
|
||||
# Folding: the value has to be measured after YAML folding in both scripts.
|
||||
mkdir -p "$FX/folded-desc"
|
||||
{
|
||||
echo "---"
|
||||
echo "name: folded-desc"
|
||||
echo "description: >"
|
||||
python3 -c "print('\n'.join([' ' + 'x' * 40] * 11))"
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "Do the thing."
|
||||
} > "$FX/folded-desc/SKILL.md"
|
||||
# Routing targets, one per tier the resolver can produce: resolves, dangles with
|
||||
# in-sentence corroboration (ERROR/FAIL), dangles alone (SUGGESTION on both
|
||||
# sides), route notation (ERROR/FAIL without corroboration), attributive
|
||||
# (silent). Each tier is here because the two scripts have to agree on the TIER,
|
||||
# not merely on the finding — a copy that promoted or demoted one of them would
|
||||
# otherwise pass this comparison.
|
||||
make_fx target-resolves "Use when doing the thing. Do not use for the other thing — use sibling-skill instead." 10
|
||||
make_fx target-dangles "Use when doing the thing. Do not use for the other thing — use sibling-skill or no-such-skill instead." 10
|
||||
make_fx target-dangles-lone "Use when doing the thing. Do not use for the other thing — use no-such-lone-skill instead." 10
|
||||
make_fx target-dangles-notation "Use when doing the thing. Do not use for the other thing — use /no-such-notation-skill instead." 10
|
||||
make_fx target-attributive "Use when doing the thing. Use pre-commit hooks instead of ad-hoc scripts." 10
|
||||
# No boundary clause at all.
|
||||
make_fx no-boundary "Use when the user wants the thing done." 10
|
||||
# A missing references/ pointer, and a present one.
|
||||
make_fx ref-missing "$CLEAN" 10
|
||||
printf '\nIf the caller needs detail, read references/absent.md first.\n' >> "$FX/ref-missing/SKILL.md"
|
||||
make_fx ref-present "$CLEAN" 10
|
||||
printf '\nIf the caller needs detail, read references/there.md first.\n' >> "$FX/ref-present/SKILL.md"
|
||||
mkdir -p "$FX/ref-present/references"
|
||||
echo "detail" > "$FX/ref-present/references/there.md"
|
||||
# Gotchas, over each guideline.
|
||||
make_fx gotchas-many "$CLEAN" 0
|
||||
cat >> "$FX/gotchas-many/SKILL.md" <<'EOF'
|
||||
|
||||
## Gotchas
|
||||
|
||||
- one trap here
|
||||
- two trap here
|
||||
- three trap here
|
||||
- four trap here
|
||||
- five trap here
|
||||
- six trap here
|
||||
- seven trap here
|
||||
|
||||
## Notes
|
||||
|
||||
EOF
|
||||
python3 -c "print(' '.join(['word'] * 200))" >> "$FX/gotchas-many/SKILL.md"
|
||||
# Gotchas over the 25% body-fraction guideline. Prose, not list items, so the
|
||||
# entry guideline cannot fire and the two suggestions stay separable: 26 section
|
||||
# words in a 100-word body is one word past the threshold.
|
||||
make_fx gotchas-fraction "$CLEAN" 0
|
||||
{
|
||||
echo ""
|
||||
echo "## Gotchas"
|
||||
echo ""
|
||||
python3 -c "print(' '.join(['word'] * 26))"
|
||||
echo ""
|
||||
echo "## Notes"
|
||||
echo ""
|
||||
python3 -c "print(' '.join(['word'] * 70))"
|
||||
} >> "$FX/gotchas-fraction/SKILL.md"
|
||||
|
||||
# The agentskills.io spec ceilings, measured over Unicode whitespace.
|
||||
#
|
||||
# These two are in the comparison at all because they used to be excluded from
|
||||
# it — `_non_adr_hook_error()` waved a spec-ceiling exit through as "not a
|
||||
# disagreement", and that exclusion is exactly why the divergence below stayed
|
||||
# invisible. The hook counted lines and words in a single awk pass (NR / NF)
|
||||
# while skill-audit counted them with Python's splitlines() / split(). The two
|
||||
# primitives do not agree: splitlines() also breaks on U+2028, U+2029, \x0b,
|
||||
# \x0c, \x1c-\x1e and \x85, and split() breaks on every Unicode space. Same
|
||||
# constants, same file, different verdict — hook green, audit FAIL, which is the
|
||||
# precise failure mode ("passes its own audit, blocked by the commit hook",
|
||||
# inverted) this whole suite exists to catch.
|
||||
#
|
||||
# One fixture per primitive, each sitting just past its ceiling on the Python
|
||||
# measurement and nowhere near it on the awk one.
|
||||
python3 - "$FX" <<'PY'
|
||||
import os
|
||||
import sys
|
||||
|
||||
fx = sys.argv[1]
|
||||
# Spelled as escapes, never as literals. An invisible separator pasted into a
|
||||
# source file is unreviewable and one editor round-trip away from becoming an
|
||||
# ordinary space, which would silently turn both fixtures into nothing.
|
||||
SEP_LINE = '\u2028' # LINE SEPARATOR: splitlines() breaks on it, awk's NR does not
|
||||
SEP_WORD = '\u00a0' # NO-BREAK SPACE: split() breaks on it, awk's NF does not
|
||||
head = ('---\nname: %s\n'
|
||||
'description: Use when doing the thing. Do not use for anything else.\n'
|
||||
'---\n\n')
|
||||
# 600 U+2028-separated segments: 605 lines to splitlines(), 6 to awk's NR.
|
||||
# Word count stays far below the 2,770 ceiling, so this fixture isolates lines.
|
||||
cases = {
|
||||
'spec-lines-u2028': SEP_LINE.join(['word'] * 600),
|
||||
# 2,800 U+00A0-separated words: 2,816 words to split(), 17 to awk's NF.
|
||||
'spec-words-u00a0': SEP_WORD.join(['word'] * 2800),
|
||||
}
|
||||
for name, body in cases.items():
|
||||
d = os.path.join(fx, name)
|
||||
os.makedirs(d, exist_ok=True)
|
||||
with open(os.path.join(d, 'SKILL.md'), 'w', encoding='utf-8') as fh:
|
||||
fh.write(head % name + body + '\n')
|
||||
PY
|
||||
|
||||
# Empty description — the shape that used to exit 0 in silence.
|
||||
mkdir -p "$FX/empty-desc"
|
||||
printf -- '---\nname: empty-desc\ndescription:\nmodel: sonnet\n---\n\nDo the thing.\n' \
|
||||
> "$FX/empty-desc/SKILL.md"
|
||||
# Every boundary shape at once, so a divergence that only appears when several
|
||||
# findings fire together is not missed.
|
||||
make_fx combined "$(desc_of_length 401)" 901
|
||||
printf '\nIf the caller needs detail, read references/absent.md first.\n' >> "$FX/combined/SKILL.md"
|
||||
|
||||
# A skill with routing targets and NO authoring root above it — deliberately
|
||||
# OUTSIDE the fixture plugin tree. Both scripts must decline out loud, and both
|
||||
# must decline identically; "both declined" is only meaningful as agreement if
|
||||
# the declining path is exercised on purpose somewhere.
|
||||
ORPHAN_ROOT="$TMPDIR_T/orphan"
|
||||
mkdir -p "$ORPHAN_ROOT/no-universe"
|
||||
{
|
||||
echo "---"
|
||||
echo "name: no-universe"
|
||||
echo "description: Use when doing the thing. Do not use for the other thing — use some-other-skill instead."
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "Do the thing."
|
||||
} > "$ORPHAN_ROOT/no-universe/SKILL.md"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The comparison
|
||||
# ---------------------------------------------------------------------------
|
||||
python3 - "$HOOK" "$SKILL_VALIDATE" "$REPO_ROOT" "$FX" "$ORPHAN_ROOT" <<'PYTHON'
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
hook, validate, repo_root, fixture_dir, orphan_dir = sys.argv[1:6]
|
||||
|
||||
passes = 0
|
||||
failures = 0
|
||||
|
||||
|
||||
def ok(msg):
|
||||
global passes
|
||||
passes += 1
|
||||
print(" PASS: %s" % msg)
|
||||
|
||||
|
||||
def bad(msg):
|
||||
global failures
|
||||
failures += 1
|
||||
print(" FAIL: %s" % msg)
|
||||
|
||||
|
||||
# Tier prefixes. The hook writes `ERROR: ` / `SUGGESTION: ` / `INFO: `; the
|
||||
# auditor writes `FAIL ` / `SUGGESTION ` / `INFO ` and additionally `PASS `
|
||||
# lines, which carry no finding and are dropped.
|
||||
TIERS = (
|
||||
('ERROR', ('ERROR:', 'FAIL ')),
|
||||
('SUGGESTION', ('SUGGESTION:', 'SUGGESTION ')),
|
||||
('INFO', ('INFO:', 'INFO ')),
|
||||
)
|
||||
|
||||
# Each rule turns a finding line into a canonical token. Wording differs between
|
||||
# the two scripts by design (one addresses a committer, the other an auditor), so
|
||||
# the tokens deliberately capture the MEASUREMENT and not the sentence.
|
||||
RULES = (
|
||||
('DESC_CHARS', re.compile(r'description is (\d+) char')),
|
||||
('BODY_WORDS', re.compile(r'body is (\d+) words')),
|
||||
('ROUTE', re.compile(r"routes to '([^']+)'")),
|
||||
('MISSING_REF', re.compile(r'points at (references/[^\s,]+)')),
|
||||
('GOTCHA_ENTRIES', re.compile(r'Gotchas section has (\d+) entries')),
|
||||
('GOTCHA_FRACTION', re.compile(r'Gotchas section is (\d+) of (\d+) body words')),
|
||||
('NO_BOUNDARY_CLAUSE', re.compile(r'(description has no boundary clause)')),
|
||||
('RESOLUTION_DECLINED', re.compile(r'(boundary-target resolution DID NOT RUN)')),
|
||||
('DESC_EMPTY', re.compile(r'(description field is missing or empty)')),
|
||||
# The agentskills.io spec ceilings. These were EXCLUDED from the comparison
|
||||
# until the awk/Python divergence shipped, on the reasoning that "the hook
|
||||
# checks whole-file lines and words" and the auditor did not. It does — with
|
||||
# the same two constants — so the exclusion was never a scope decision, only
|
||||
# an untested assumption, and it hid a real disagreement. Both scripts spell
|
||||
# the finding differently, so the patterns match either wording and capture
|
||||
# only the MEASUREMENT:
|
||||
# hook: "... has 605 lines, exceeding the 500-line ceiling ..."
|
||||
# audit: "SKILL.md line count 605 — exceeds 500-line limit"
|
||||
('SPEC_LINES', re.compile(r'(?:has|line count) (\d+)(?: lines,)? (?:exceeding|—)')),
|
||||
('SPEC_WORDS', re.compile(
|
||||
r'(?:has|word count) (\d+)(?: words \(proxy for tokens\),)? (?:exceeding|—)')),
|
||||
)
|
||||
|
||||
|
||||
def verdict(output):
|
||||
"""The set of ADR-0020 findings in a script's output, tier included.
|
||||
|
||||
Lines that match no rule are dropped rather than compared: the two scripts
|
||||
legitimately check different things outside ADR-0020 (name/directory
|
||||
agreement, script executability, the 1024-char spec backstop), and forcing
|
||||
those into the comparison would report a difference that is not a
|
||||
disagreement.
|
||||
|
||||
The whole-file line and word ceilings are NOT in that list. They were
|
||||
excluded once, on the untested assumption that awk and splitlines() agree;
|
||||
they do not, and the divergence was invisible for exactly as long as the
|
||||
exclusion stood. SPEC_LINES/SPEC_WORDS are compared like any other rule —
|
||||
see the file header. Do not re-add an exclusion for them.
|
||||
"""
|
||||
found = set()
|
||||
for raw in output.splitlines():
|
||||
line = raw.strip()
|
||||
tier = None
|
||||
for name, prefixes in TIERS:
|
||||
if any(line.startswith(p) for p in prefixes):
|
||||
tier = name
|
||||
break
|
||||
if tier is None:
|
||||
continue
|
||||
for token, pattern in RULES:
|
||||
match = pattern.search(line)
|
||||
if match:
|
||||
found.add((tier, token) + tuple(match.groups()))
|
||||
return found
|
||||
|
||||
|
||||
def run(cmd):
|
||||
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
return proc.returncode, proc.stdout.decode('utf-8', 'replace')
|
||||
|
||||
|
||||
def compare(label, skill_dir):
|
||||
skill_md = os.path.join(skill_dir, 'SKILL.md')
|
||||
hook_rc, hook_out = run(['bash', hook, skill_md])
|
||||
audit_rc, audit_out = run(['bash', validate, skill_dir])
|
||||
hook_v = verdict(hook_out)
|
||||
audit_v = verdict(audit_out)
|
||||
|
||||
problems = []
|
||||
only_hook = sorted(hook_v - audit_v)
|
||||
only_audit = sorted(audit_v - hook_v)
|
||||
if only_hook:
|
||||
problems.append('only the hook reported %s' % (only_hook,))
|
||||
if only_audit:
|
||||
problems.append('only skill-audit reported %s' % (only_audit,))
|
||||
|
||||
# Exit codes are compared on the ADR-0020 axis only: an ERROR-tier ADR-0020
|
||||
# finding must make BOTH scripts non-zero, and neither may be turned
|
||||
# non-zero by a SUGGESTION. The raw codes cannot be compared directly —
|
||||
# validate.sh also fails on checks the hook does not run at all.
|
||||
hook_err = any(t == 'ERROR' for t, *_ in hook_v)
|
||||
audit_err = any(t == 'ERROR' for t, *_ in audit_v)
|
||||
if hook_err and hook_rc == 0:
|
||||
problems.append('the hook reported an ADR-0020 ERROR but exited 0')
|
||||
if audit_err and audit_rc == 0:
|
||||
problems.append('skill-audit reported an ADR-0020 FAIL but exited 0')
|
||||
# No escape hatch here any more. There used to be one — a
|
||||
# `_non_adr_hook_error()` helper that waved through a non-zero hook exit
|
||||
# explained by MAX_LINES / MAX_WORDS, on the grounds that those two were
|
||||
# outside the comparison. They are inside it now (see SPEC_LINES /
|
||||
# SPEC_WORDS in RULES), so every ERROR the hook can raise is a token this
|
||||
# comparison holds both scripts to.
|
||||
if not hook_err and hook_rc != 0:
|
||||
problems.append('the hook exited %d with no compared ERROR at all — it has an '
|
||||
'ERROR source this comparison does not know about' % hook_rc)
|
||||
|
||||
if problems:
|
||||
bad('%s: %s' % (label, '; '.join(problems)))
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# --- The real corpus -------------------------------------------------------
|
||||
corpus = sorted(glob.glob(os.path.join(repo_root, 'plugins', '*', '.apm', 'skills', '*')))
|
||||
corpus = [d for d in corpus if os.path.isfile(os.path.join(d, 'SKILL.md'))]
|
||||
print("")
|
||||
print("--- the two scripts agree on every skill in the live corpus (%d files) ---" % len(corpus))
|
||||
if len(corpus) < 30:
|
||||
bad('only %d corpus skills were discovered — the glob is wrong, so this leg '
|
||||
'proves nothing' % len(corpus))
|
||||
else:
|
||||
ok('discovered %d corpus skills to compare' % len(corpus))
|
||||
agreed = 0
|
||||
for skill_dir in corpus:
|
||||
rel = os.path.relpath(skill_dir, repo_root)
|
||||
if compare(rel, skill_dir):
|
||||
agreed += 1
|
||||
if agreed == len(corpus):
|
||||
ok('all %d corpus skills produce identical ADR-0020 verdicts from both scripts' % agreed)
|
||||
|
||||
# --- Boundary fixtures -----------------------------------------------------
|
||||
fixtures = sorted(d for d in (glob.glob(os.path.join(fixture_dir, '*'))
|
||||
+ glob.glob(os.path.join(orphan_dir, '*')))
|
||||
if os.path.isfile(os.path.join(d, 'SKILL.md')))
|
||||
print("")
|
||||
print("--- the two scripts agree on every boundary fixture (%d files) ---" % len(fixtures))
|
||||
if len(fixtures) < 15:
|
||||
bad('only %d fixtures were built — the fixture set is incomplete, so the '
|
||||
'boundaries the corpus does not cover are untested' % len(fixtures))
|
||||
else:
|
||||
ok('built %d boundary fixtures to compare' % len(fixtures))
|
||||
fx_agreed = 0
|
||||
for skill_dir in fixtures:
|
||||
if compare(os.path.basename(skill_dir), skill_dir):
|
||||
fx_agreed += 1
|
||||
if fx_agreed == len(fixtures):
|
||||
ok('all %d boundary fixtures produce identical ADR-0020 verdicts from both scripts' % fx_agreed)
|
||||
|
||||
# --- The comparison must not be vacuous ------------------------------------
|
||||
# Everything above would also pass if verdict() extracted nothing at all. So the
|
||||
# fixtures are required to have produced findings across every axis this suite
|
||||
# claims to compare — if a rule stops matching (a reworded message, say), that is
|
||||
# a silent loss of coverage and it fails here instead.
|
||||
print("")
|
||||
print("--- the comparison actually extracted findings on every axis it claims to cover ---")
|
||||
seen_tokens = set()
|
||||
for skill_dir in fixtures:
|
||||
_, out = run(['bash', hook, os.path.join(skill_dir, 'SKILL.md')])
|
||||
for entry in verdict(out):
|
||||
seen_tokens.add(entry[1])
|
||||
_, out = run(['bash', validate, skill_dir])
|
||||
for entry in verdict(out):
|
||||
seen_tokens.add(entry[1])
|
||||
expected_tokens = {t for t, _ in RULES}
|
||||
missing = sorted(expected_tokens - seen_tokens)
|
||||
if missing:
|
||||
bad('no fixture produced a finding for %s — verdict() may no longer match '
|
||||
'those messages, and any disagreement on them would go unseen' % missing)
|
||||
else:
|
||||
ok('every one of the %d compared axes was exercised by at least one fixture'
|
||||
% len(expected_tokens))
|
||||
|
||||
# --- The Unicode-whitespace fixtures, named and asserted directly -----------
|
||||
# The two comparisons above would catch this divergence, but only as "fixture
|
||||
# spec-lines-u2028 disagreed" — one line among 65. Spelled out here so the
|
||||
# failure names the primitive, and so the ceiling is asserted to FIRE in both
|
||||
# scripts rather than merely to be reported the same way by both.
|
||||
print("")
|
||||
print("--- both scripts break the spec ceilings on the same Unicode whitespace ---")
|
||||
for name, token, expected in (('spec-lines-u2028', 'SPEC_LINES', '605'),
|
||||
('spec-words-u00a0', 'SPEC_WORDS', '2816')):
|
||||
skill_dir = os.path.join(fixture_dir, name)
|
||||
_, h_out = run(['bash', hook, os.path.join(skill_dir, 'SKILL.md')])
|
||||
_, a_out = run(['bash', validate, skill_dir])
|
||||
want = ('ERROR', token, expected)
|
||||
missing = [who for who, v in (('the hook', verdict(h_out)),
|
||||
('skill-audit', verdict(a_out)))
|
||||
if want not in v]
|
||||
if missing:
|
||||
bad('%s: %s did not report %s=%s. The two scripts must count with the '
|
||||
'same primitive — Python splitlines()/split(), not awk NR/NF, which '
|
||||
'does not break on this character' % (name, ' and '.join(missing),
|
||||
token, expected))
|
||||
else:
|
||||
ok('%s: both scripts measure %s=%s and raise the ceiling ERROR'
|
||||
% (name, token, expected))
|
||||
|
||||
print("")
|
||||
print("Results: %d passed, %d failed" % (passes, failures))
|
||||
sys.exit(1 if failures else 0)
|
||||
PYTHON
|
||||
Executable
+440
@@ -0,0 +1,440 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression test for the two ways an ADR-0020 gate can be made to check NOTHING
|
||||
# while still exiting 0. Both were live defects, both were silent, and both sit
|
||||
# in the shared resolver block that all three scripts embed verbatim — so every
|
||||
# case below runs against all three.
|
||||
#
|
||||
# 1. THE FRONTMATTER BLOCKER. The frontmatter matcher used to be `^---\n`. A
|
||||
# UTF-8 BOM, a leading blank line, a trailing space after either marker, or
|
||||
# CRLF line endings all defeated it, and the miss was not reported: every
|
||||
# ADR-0020 check was skipped and the file passed. Measured at the time: a
|
||||
# 550-character description with a 1,000-word body exited 0 behind a BOM.
|
||||
# So this file asserts two complementary things — that each of those four
|
||||
# shapes is now TOLERATED (the findings actually fire), and that
|
||||
# frontmatter which genuinely cannot be parsed is a hard ERROR rather than
|
||||
# a quiet skip. A file that cannot be measured must never report green.
|
||||
#
|
||||
# 2. THE VALUELESS DESCRIPTION. `description:` with no value, followed by
|
||||
# another key, let a line regex's `\s*` cross the newline and capture the
|
||||
# NEXT key. The value then looked present (so "missing or empty" never
|
||||
# fired) and was empty once folded (so every ADR-0020 gate early-returned).
|
||||
# An agent file with one exited 0 with zero output through a BLOCKING
|
||||
# pre-push gate. All five spellings of "no value" are pinned here, plus the
|
||||
# three shapes where the value is present but is not TEXT — a list, a
|
||||
# mapping, a bool. Those used to be `str()`-coerced and then measured as a
|
||||
# Python repr, so `description: true` was the four-character "True" and
|
||||
# passed the 400-character gate.
|
||||
#
|
||||
# 3. THE INDENTED CLOSING MARKER. The mirror image of (1): content the pattern
|
||||
# was too LOOSE to reject. `\r?\n[ \t]*---` matched an indented `---` inside
|
||||
# a `>`-folded description, truncating the frontmatter mid-value — the
|
||||
# description gate then measured a fragment and the body gate measured the
|
||||
# discarded description text.
|
||||
#
|
||||
# Every needle names the specific branch or measurement the case is about. A
|
||||
# needle loose enough to match two branches is how the yaml-none fixture spent
|
||||
# its life asserting the wrong one: it emitted `---\n---\n`, which never matched
|
||||
# the frontmatter pattern at all, and passed on the bare word "frontmatter".
|
||||
#
|
||||
# Both fixtures carry an over-ceiling description AND an over-ceiling body on
|
||||
# purpose: asserting a non-zero exit alone would be satisfied by the "cannot
|
||||
# parse" error itself, so the tolerated shapes are asserted on the CONTENT of
|
||||
# the findings, not on the exit code.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
HOOK="$REPO_ROOT/scripts/skill-size-check.sh"
|
||||
SKILL_VALIDATE="$REPO_ROOT/plugins/kyberforge/.apm/skills/skill-audit/scripts/validate.sh"
|
||||
AGENT_VALIDATE="$REPO_ROOT/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate.sh"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
TMPDIR_T="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMPDIR_T"' EXIT
|
||||
|
||||
DESC_CHARS=450
|
||||
BODY_WORDS=1000
|
||||
|
||||
# write_fixture <kind> <path> <name> — one generator for both file shapes.
|
||||
#
|
||||
# Byte-level control is the point: BOM placement, line endings and trailing
|
||||
# whitespace are exactly what is under test, so the file is emitted in binary
|
||||
# mode rather than through a shell heredoc that would normalise them.
|
||||
write_fixture() {
|
||||
python3 - "$1" "$2" "$3" "$DESC_CHARS" "$BODY_WORDS" <<'PY'
|
||||
import sys
|
||||
|
||||
kind, path, name, desc_chars, body_words = sys.argv[1:6]
|
||||
desc = 'x' * int(desc_chars)
|
||||
body = ' '.join(['word'] * int(body_words))
|
||||
|
||||
# The default, well-formed shape. Variants below mutate it.
|
||||
open_marker = '---'
|
||||
close_marker = '---'
|
||||
prefix = ''
|
||||
newline = '\n'
|
||||
fm_lines = ['name: ' + name, 'description: ' + desc]
|
||||
|
||||
if kind == 'plain':
|
||||
pass
|
||||
elif kind == 'bom':
|
||||
prefix = ''
|
||||
elif kind == 'leading-blanks':
|
||||
prefix = '\n\n \n'
|
||||
elif kind == 'trailing-ws':
|
||||
open_marker = '--- '
|
||||
close_marker = '---\t '
|
||||
elif kind == 'crlf':
|
||||
newline = '\r\n'
|
||||
elif kind == 'no-close':
|
||||
close_marker = None
|
||||
elif kind == 'yaml-list':
|
||||
fm_lines = ['- one', '- two']
|
||||
elif kind == 'yaml-string':
|
||||
fm_lines = ['just a bare scalar, not a mapping']
|
||||
elif kind == 'yaml-none':
|
||||
# A comment-only block, NOT an empty one. `---\n---\n` does not match
|
||||
# FRONTMATTER_RE at all (the pattern needs a `\n` between the markers), so
|
||||
# it lands on the "no parseable frontmatter" branch and never reaches the
|
||||
# `data is None` -> "not a YAML mapping" branch this fixture is named for.
|
||||
# It passed anyway because the needle used to be the bare word
|
||||
# "frontmatter", which both messages contain. A comment is real frontmatter
|
||||
# text that yaml.safe_load() returns None for, which is the branch.
|
||||
fm_lines = ['# nothing but a comment']
|
||||
elif kind == 'yaml-empty-block':
|
||||
# The shape the fixture above USED to have, kept as its own case so the
|
||||
# "no parseable frontmatter block" branch is covered on purpose rather than
|
||||
# by accident.
|
||||
fm_lines = []
|
||||
elif kind == 'desc-folded-indented':
|
||||
# A `>`-folded description whose CONTENT contains an indented `---` line.
|
||||
# YAML block-scalar content must be indented deeper than its key, so this is
|
||||
# a value, not a document marker — but the closing pattern used to be
|
||||
# `\r?\n[ \t]*---`, which matched it, truncated the frontmatter mid-value
|
||||
# and silently reclassified the rest of the description as body. Both halves
|
||||
# of that are vacuous greens: the description gate measured a fragment, and
|
||||
# the body gate measured description text.
|
||||
#
|
||||
# The value is padded to exactly desc_chars AFTER folding, and the boundary
|
||||
# clause naming a target sits in the part the truncation used to discard.
|
||||
head = 'Use when doing the thing. '
|
||||
tail = ' Do not use for improvements — use no-such-folded-target instead.'
|
||||
span = int(desc_chars) - len(head) - len(tail) - len(' --- ')
|
||||
if span < 2:
|
||||
raise SystemExit('desc_chars too small for the folded fixture')
|
||||
fm_lines = [
|
||||
'name: ' + name,
|
||||
'description: >',
|
||||
' ' + head + 'x' * (span // 2),
|
||||
' ---',
|
||||
' ' + 'x' * (span - span // 2) + tail,
|
||||
]
|
||||
elif kind == 'desc-list':
|
||||
fm_lines = ['name: ' + name, 'description:', ' - one', ' - two']
|
||||
elif kind == 'desc-mapping':
|
||||
fm_lines = ['name: ' + name, 'description:', ' text: a description']
|
||||
elif kind == 'desc-bool':
|
||||
fm_lines = ['name: ' + name, 'description: true']
|
||||
elif kind == 'yaml-malformed':
|
||||
fm_lines = ['name: ' + name, 'description: "unterminated', 'tabs:\t- a']
|
||||
elif kind == 'desc-no-value':
|
||||
# The exact shape that exited 0 with zero output: a line regex's `\s*`
|
||||
# crosses the newline and captures `model: sonnet` as the description.
|
||||
fm_lines = ['name: ' + name, 'description:', 'model: sonnet']
|
||||
elif kind == 'desc-null':
|
||||
fm_lines = ['name: ' + name, 'description: null']
|
||||
elif kind == 'desc-single-quoted-empty':
|
||||
fm_lines = ['name: ' + name, "description: ''"]
|
||||
elif kind == 'desc-double-quoted-empty':
|
||||
fm_lines = ['name: ' + name, 'description: ""']
|
||||
elif kind == 'desc-empty-fold':
|
||||
fm_lines = ['name: ' + name, 'description: >']
|
||||
else:
|
||||
raise SystemExit('unknown fixture kind: %s' % kind)
|
||||
|
||||
parts = [prefix, open_marker, newline]
|
||||
for line in fm_lines:
|
||||
parts.append(line)
|
||||
parts.append(newline)
|
||||
if close_marker is not None:
|
||||
parts.append(close_marker)
|
||||
parts.append(newline)
|
||||
parts.append(newline)
|
||||
parts.append(body)
|
||||
parts.append(newline)
|
||||
|
||||
with open(path, 'wb') as fh:
|
||||
fh.write(''.join(parts).encode('utf-8'))
|
||||
PY
|
||||
}
|
||||
|
||||
# Builds all three subjects for one fixture kind and echoes nothing; the paths
|
||||
# are fixed by convention so the probes below can find them.
|
||||
#
|
||||
# skill-audit takes a DIRECTORY (SKILL.md inside it, name matching the dir);
|
||||
# agent-audit takes a FILE inside an apm package. The hook takes the SKILL.md
|
||||
# directly, so it and skill-audit share one file.
|
||||
build_subjects() {
|
||||
local kind="$1" base="$TMPDIR_T/$1"
|
||||
rm -rf "$base"
|
||||
mkdir -p "$base/skill/my-skill" "$base/agent/.apm/agents"
|
||||
cat > "$base/agent/apm.yml" <<'EOF'
|
||||
name: test-package
|
||||
version: 0.1.0
|
||||
type: skill
|
||||
EOF
|
||||
write_fixture "$kind" "$base/skill/my-skill/SKILL.md" my-skill
|
||||
write_fixture "$kind" "$base/agent/.apm/agents/my-agent.agent.md" my-agent
|
||||
}
|
||||
|
||||
# probe_all <label> <kind> <needle>... — runs all three scripts over the fixture
|
||||
# and requires every one of them to exit non-zero AND report every needle. One
|
||||
# assertion per script would let two of them drift apart while the suite stayed
|
||||
# green; the whole point of the shared resolver block is that they cannot.
|
||||
#
|
||||
# A needle written `@skills:<text>` is asserted for the hook and skill-audit but
|
||||
# NOT for agent-audit. There is exactly one such needle in this file — the body
|
||||
# word ceiling — and the exemption is the ADR, not a workaround: ADR-0020 gives
|
||||
# agents the description gates and deliberately NO body word gate, because an
|
||||
# agent body becomes the system prompt of a fresh context rather than competing
|
||||
# with the caller's live conversation. Demanding a body finding from agent-audit
|
||||
# would be demanding the ADR be contradicted.
|
||||
probe_all() {
|
||||
local label="$1" kind="$2"
|
||||
shift 2
|
||||
local base="$TMPDIR_T/$kind"
|
||||
local -a targets=(
|
||||
"hook|$HOOK|$base/skill/my-skill/SKILL.md"
|
||||
"skill-audit|$SKILL_VALIDATE|$base/skill/my-skill"
|
||||
"agent-audit|$AGENT_VALIDATE|$base/agent/.apm/agents/my-agent.agent.md"
|
||||
)
|
||||
local problems=""
|
||||
for target in "${targets[@]}"; do
|
||||
local who="${target%%|*}" rest="${target#*|}"
|
||||
local script="${rest%%|*}" arg="${rest#*|}"
|
||||
local out status=0
|
||||
set +e
|
||||
out="$(bash "$script" "$arg" 2>&1)"
|
||||
status=$?
|
||||
set -e
|
||||
if [[ $status -eq 0 ]]; then
|
||||
problems="$problems [$who exited 0: ${out:-<no output>}]"
|
||||
continue
|
||||
fi
|
||||
for needle in "$@"; do
|
||||
if [[ "$needle" == @skills:* ]]; then
|
||||
# Spelled as a full `if`, not `[[ ... ]] && continue`. Under `set -e` the
|
||||
# short form's exit status is the test's when it is false, and relying on
|
||||
# the &&-list exemption to keep that from aborting the run is a footgun
|
||||
# one edit away from biting.
|
||||
if [[ "$who" == agent-audit ]]; then
|
||||
continue
|
||||
fi
|
||||
needle="${needle#@skills:}"
|
||||
fi
|
||||
if [[ "$out" != *"$needle"* ]]; then
|
||||
problems="$problems [$who never said '$needle': $out]"
|
||||
fi
|
||||
done
|
||||
done
|
||||
if [[ -z "$problems" ]]; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label —$problems"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1a. Tolerated frontmatter shapes — the gates must RUN, not merely not-pass
|
||||
# ---------------------------------------------------------------------------
|
||||
# The needles are the FINDINGS, not the exit code. A script that rejected the BOM
|
||||
# outright would exit non-zero too, and would still be skipping every ADR-0020
|
||||
# measurement — which is the defect, one error message later.
|
||||
echo ""
|
||||
echo "--- a BOM, leading blanks, trailing marker whitespace and CRLF are all tolerated, and the gates still fire ---"
|
||||
for kind in plain bom leading-blanks trailing-ws crlf; do
|
||||
build_subjects "$kind"
|
||||
done
|
||||
probe_all "control: a well-formed over-ceiling file fails on BOTH the description and the body" \
|
||||
plain "description is $DESC_CHARS char" "@skills:body is $BODY_WORDS words"
|
||||
probe_all "a UTF-8 BOM does not hide an over-ceiling description or body" \
|
||||
bom "description is $DESC_CHARS char" "@skills:body is $BODY_WORDS words"
|
||||
probe_all "leading blank lines before the opening --- do not hide the findings" \
|
||||
leading-blanks "description is $DESC_CHARS char" "@skills:body is $BODY_WORDS words"
|
||||
probe_all "trailing whitespace after either --- marker does not hide the findings" \
|
||||
trailing-ws "description is $DESC_CHARS char" "@skills:body is $BODY_WORDS words"
|
||||
# Note on what this last one can and cannot detect. read_text() opens the file in
|
||||
# TEXT mode, so Python's universal-newline translation turns \r\n into \n before
|
||||
# the frontmatter matcher ever sees it — verified by mutation: reverting
|
||||
# FRONTMATTER_RE to the old `^---\n(.*?)\n---` breaks the leading-blanks and
|
||||
# trailing-whitespace cases above but NOT this one. So this case pins the
|
||||
# end-to-end behaviour (a CRLF file is measured, not skipped) rather than the
|
||||
# `\r?\n` alternations in the regex, and it would catch a future switch to binary
|
||||
# reads or to a newline='' open. Kept for that reason, and labelled so nobody
|
||||
# reads it as covering more than it does.
|
||||
probe_all "CRLF line endings do not hide the findings" \
|
||||
crlf "description is $DESC_CHARS char" "@skills:body is $BODY_WORDS words"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1a-bis. An indented `---` inside a block scalar is CONTENT, not a marker
|
||||
# ---------------------------------------------------------------------------
|
||||
# The mirror image of the four shapes above. Those were markers the pattern was
|
||||
# too strict to accept; this is content the pattern was too loose to reject. The
|
||||
# closing marker used to be `\r?\n[ \t]*---`, so an indented `---` inside a
|
||||
# `>`-folded description ended the frontmatter early: the description gate then
|
||||
# measured a truncated fragment (under every ceiling, so silent) and the body
|
||||
# gate measured the discarded description text as body. Measured on the fixture
|
||||
# below, the old code exited 0 with nothing but a spurious "no boundary clause"
|
||||
# SUGGESTION — the clause is in the half it threw away.
|
||||
#
|
||||
# The needle is the full-value length, so a script that merely rejected the file
|
||||
# would not satisfy it.
|
||||
echo ""
|
||||
echo "--- an indented --- inside a >-folded description is content, not the end of the frontmatter ---"
|
||||
build_subjects desc-folded-indented
|
||||
probe_all "a folded description containing an indented '---' is measured whole" \
|
||||
desc-folded-indented "description is $DESC_CHARS char"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1b. Unparseable frontmatter is a hard ERROR, never a quiet skip
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "--- genuinely unparseable frontmatter exits non-zero with a message, rather than passing quietly ---"
|
||||
# Each needle names the BRANCH the fixture is supposed to reach, not the word
|
||||
# "frontmatter" — which every one of these messages contains, and which is why
|
||||
# the yaml-none fixture below passed for years while landing on the wrong branch
|
||||
# entirely.
|
||||
for kind in no-close yaml-list yaml-string yaml-none yaml-empty-block yaml-malformed; do
|
||||
build_subjects "$kind"
|
||||
done
|
||||
probe_all "frontmatter with no closing --- is reported, not skipped" \
|
||||
no-close "parseable YAML frontmatter block"
|
||||
probe_all "frontmatter that parses to a LIST is reported, not skipped" \
|
||||
yaml-list "frontmatter is not a YAML mapping"
|
||||
probe_all "frontmatter that parses to a STRING is reported, not skipped" \
|
||||
yaml-string "frontmatter is not a YAML mapping"
|
||||
probe_all "frontmatter that parses to None (a comment-only block) is reported, not skipped" \
|
||||
yaml-none "frontmatter is not a YAML mapping"
|
||||
probe_all "a completely empty '---/---' block is reported, not skipped" \
|
||||
yaml-empty-block "parseable YAML frontmatter block"
|
||||
# Two needles, both naming the SYNTAX branch specifically. "frontmatter is not
|
||||
# valid YAML" is now exclusive to it — the wrong-typed-description failures reach
|
||||
# the same wrapper and no longer borrow that phrase (see 2c below) — and the
|
||||
# scanner context proves the parser's own diagnostic survives the wrapper rather
|
||||
# than being replaced by a generic one. Do not needle the tail of PyYAML's
|
||||
# message: an earlier attempt used "could not find expected", which PyYAML 6.0.3
|
||||
# does not emit for this fixture at all, so the case failed on the assertion
|
||||
# rather than on the behaviour.
|
||||
probe_all "malformed YAML in the frontmatter is reported, not skipped" \
|
||||
yaml-malformed "frontmatter is not valid YAML" "while scanning a quoted scalar"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. A valueless description is a hard FAIL in all three scripts
|
||||
# ---------------------------------------------------------------------------
|
||||
# All five spellings mean the same thing to a YAML parser — an empty value — and
|
||||
# all five have to be decided on the FOLDED value rather than on a line regex.
|
||||
# `description:` followed by `model: sonnet` is the one that shipped: it made the
|
||||
# value look present, skipped the "missing or empty" failure, and then
|
||||
# early-returned out of every ADR-0020 gate on the genuinely empty folded value.
|
||||
echo ""
|
||||
echo "--- every spelling of a valueless description hard-FAILs in all three scripts ---"
|
||||
for kind in desc-no-value desc-null desc-single-quoted-empty desc-double-quoted-empty desc-empty-fold; do
|
||||
build_subjects "$kind"
|
||||
done
|
||||
probe_all "'description:' with no value (next key not captured as the value) FAILs" \
|
||||
desc-no-value "description field is missing or empty"
|
||||
probe_all "'description: null' FAILs" \
|
||||
desc-null "description field is missing or empty"
|
||||
probe_all "\"description: ''\" FAILs" \
|
||||
desc-single-quoted-empty "description field is missing or empty"
|
||||
probe_all "'description: \"\"' FAILs" \
|
||||
desc-double-quoted-empty "description field is missing or empty"
|
||||
probe_all "'description: >' with nothing folded under it FAILs" \
|
||||
desc-empty-fold "description field is missing or empty"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2b. A description that is not a STRING is a parse failure, not a measurement
|
||||
# ---------------------------------------------------------------------------
|
||||
# The other half of the same family, and the reason it belongs beside the five
|
||||
# above: all eight shapes are "the description is not a description", and seven
|
||||
# of them used to be handled while this one was silently coerced. A non-string
|
||||
# value went through `str()` and was then measured as a Python repr —
|
||||
# `description: true` became the four-character "True" and sailed through the
|
||||
# 400-character gate, a list became "['one', 'two']", a mapping its dict repr.
|
||||
# None of those is text a host can preload, so measuring one is a green verdict
|
||||
# on a file that was never measured.
|
||||
echo ""
|
||||
echo "--- a description that is a list, a mapping or a bool hard-FAILs in all three scripts ---"
|
||||
for kind in desc-list desc-mapping desc-bool; do
|
||||
build_subjects "$kind"
|
||||
done
|
||||
probe_all "a LIST description FAILs rather than being measured as its repr" \
|
||||
desc-list "description is a list, not a string"
|
||||
probe_all "a MAPPING description FAILs rather than being measured as its repr" \
|
||||
desc-mapping "description is a dict, not a string"
|
||||
probe_all "a BOOL description FAILs rather than being measured as the 4-char 'True'" \
|
||||
desc-bool "description is a bool, not a string"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2c. The FAILURE CLASS reported has to be the one that happened
|
||||
# ---------------------------------------------------------------------------
|
||||
# The three fixtures above reach the same wrapper as a genuine YAML syntax
|
||||
# error, and that wrapper used to prefix a hard-coded "frontmatter is not valid
|
||||
# YAML (...)" onto all of them. For a non-string description that is false: the
|
||||
# block parses, only the field's TYPE is wrong. On a blocking gate with no
|
||||
# baseline it sent the author hunting for a syntax error that is not there. The
|
||||
# assertion runs in both directions, because fixing it by dropping the phrase
|
||||
# everywhere would trade one wrong diagnosis for another.
|
||||
echo ""
|
||||
echo "--- 'not valid YAML' is said for a syntax error and NOT for a wrong-typed description ---"
|
||||
YAML_CLASS_PROBLEMS=""
|
||||
for spec in "yaml-malformed|yes" "desc-list|no" "desc-mapping|no" "desc-bool|no"; do
|
||||
kind="${spec%%|*}"
|
||||
want="${spec#*|}"
|
||||
build_subjects "$kind"
|
||||
for target in \
|
||||
"hook|$HOOK|$TMPDIR_T/$kind/skill/my-skill/SKILL.md" \
|
||||
"skill-audit|$SKILL_VALIDATE|$TMPDIR_T/$kind/skill/my-skill" \
|
||||
"agent-audit|$AGENT_VALIDATE|$TMPDIR_T/$kind/agent/.apm/agents/my-agent.agent.md"
|
||||
do
|
||||
who="${target%%|*}"; rest="${target#*|}"
|
||||
script="${rest%%|*}"; arg="${rest#*|}"
|
||||
set +e
|
||||
out="$(bash "$script" "$arg" 2>&1)"
|
||||
set -e
|
||||
if [[ "$want" == yes && "$out" != *"frontmatter is not valid YAML"* ]]; then
|
||||
YAML_CLASS_PROBLEMS="$YAML_CLASS_PROBLEMS [$who did not call $kind a YAML syntax error: $out]"
|
||||
fi
|
||||
if [[ "$want" == no && "$out" == *"not valid YAML"* ]]; then
|
||||
YAML_CLASS_PROBLEMS="$YAML_CLASS_PROBLEMS [$who called $kind invalid YAML, but the frontmatter parsed: $out]"
|
||||
fi
|
||||
done
|
||||
done
|
||||
if [[ -z "$YAML_CLASS_PROBLEMS" ]]; then
|
||||
pass "a type error is reported as a type error and a syntax error as a syntax error"
|
||||
else
|
||||
fail "wrong failure class reported —$YAML_CLASS_PROBLEMS"
|
||||
fi
|
||||
|
||||
# The specific regression, spelled out: the valueless-description agent file must
|
||||
# not merely fail — it must not be SILENT. Zero output on a blocking gate is what
|
||||
# made this un-diagnosable, so the output is asserted non-empty independently.
|
||||
echo ""
|
||||
echo "--- the valueless-description agent file produces output, not silence ---"
|
||||
build_subjects desc-no-value
|
||||
set +e
|
||||
SILENT_OUT="$(bash "$AGENT_VALIDATE" "$TMPDIR_T/desc-no-value/agent/.apm/agents/my-agent.agent.md" 2>&1)"
|
||||
SILENT_RC=$?
|
||||
set -e
|
||||
if [[ $SILENT_RC -ne 0 && -n "$SILENT_OUT" ]]; then
|
||||
pass "agent-audit reports a valueless description rather than exiting 0 with zero output"
|
||||
else
|
||||
fail "agent-audit exited $SILENT_RC with output '${SILENT_OUT:-<empty>}' — the original defect was exit 0 and total silence on a blocking pre-push gate"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
Executable
+610
@@ -0,0 +1,610 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression test for the two properties of ADR-0020 boundary-target resolution
|
||||
# that decide whether the gate can be trusted at all.
|
||||
#
|
||||
# 1. MACHINE INDEPENDENCE. The resolution universe is derived by walking up
|
||||
# FROM THE TARGET FILE to an authoring root, and when one is found the
|
||||
# deployed .claude/ and .agents/ trees are deliberately NOT consulted. Those
|
||||
# trees are `apm install` output — gitignored, and present only on a machine
|
||||
# that has run it. Four cross-plugin targets in this repo resolved through
|
||||
# .claude/skills/ alone, so the same commit measured 2 dangling targets on a
|
||||
# developer machine and 6 on a fresh clone. A gate shipping hot with no
|
||||
# baseline cannot give two answers, so this file asserts the verdict is
|
||||
# identical with and without a deployed tree — on a synthetic fixture AND on
|
||||
# the real 39-skill corpus.
|
||||
#
|
||||
# Three further ways the universe can be built out of the wrong directory,
|
||||
# each of which shipped: a `.git` at the CONSUMER root (the fallback is
|
||||
# truthy in any git repo, which made the deployed-tree branch dead code), a
|
||||
# `.git` INSIDE a plugin (the walk-up is two passes precisely so this cannot
|
||||
# capture the root), and glob metacharacters in the checkout path (which
|
||||
# turned the directory name into a character class matching nothing, and the
|
||||
# resolver into a no-op that still reported green).
|
||||
#
|
||||
# 2. THE BARE-TARGET GRAMMAR RULE. A hyphenated token used as a compound
|
||||
# MODIFIER ("pre-commit hooks", "pull-request template") is prose, not a
|
||||
# route; a terminal one is a real target. Getting this wrong in either
|
||||
# direction is fatal: firing on prose makes the gate untrustworthy and it
|
||||
# gets turned off, while suppressing too much deletes the only two true
|
||||
# positives the corpus has. Both live true positives are BARE, which is why
|
||||
# the rule keys on the FOLLOWER TOKEN rather than on marking, and why they
|
||||
# are pinned by name below — a future false-positive fix must not be able to
|
||||
# quietly take them with it.
|
||||
#
|
||||
# 3. IN-SENTENCE CORROBORATION. Terminal position alone is not evidence of a
|
||||
# route: "run `pre-commit` instead", "see `commit-msg`", "use the clean-up
|
||||
# instead" and "run unit-tests" are all terminal, all prose, and all were
|
||||
# hard FAILs with no suppression mechanism anywhere in the gate. A
|
||||
# prose-form target therefore blocks only when its own sentence names
|
||||
# another target that RESOLVES; otherwise it is reported at SUGGESTION tier
|
||||
# and the commit proceeds. Route NOTATION (`/name`, `-> name`) is exempt
|
||||
# and always blocks. Both halves are asserted below: the prose class must
|
||||
# report-not-block, and the notation and corroborated forms must still
|
||||
# ERROR, or the fix would have eaten the gate rather than narrowed it.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
HOOK="$REPO_ROOT/scripts/skill-size-check.sh"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
TMPDIR_T="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMPDIR_T"' EXIT
|
||||
|
||||
# write_skill <skill-dir> <name> <desc>
|
||||
write_skill() {
|
||||
mkdir -p "$1"
|
||||
{
|
||||
echo "---"
|
||||
echo "name: $2"
|
||||
echo "description: $3"
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "Do the thing."
|
||||
} > "$1/SKILL.md"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Machine independence — synthetic fixture
|
||||
# ---------------------------------------------------------------------------
|
||||
# Two trees, identical except that one also carries a deployed .claude/ tree
|
||||
# holding a skill and an agent that exist NOWHERE in plugins/. The subject routes
|
||||
# to one name that lives in a sibling plugin (must resolve in both) and one that
|
||||
# lives only in .claude/ (must DANGLE in both — an authoring root exists, so the
|
||||
# deployed tree is not part of the universe).
|
||||
#
|
||||
# If the deployed tree were consulted, the second target would resolve on the
|
||||
# machine that has run `apm install` and dangle on a fresh clone. That is the
|
||||
# 2-vs-6 defect exactly, at fixture scale.
|
||||
echo ""
|
||||
echo "--- the same file gets the same verdict with and without a deployed .claude/ tree ---"
|
||||
build_tree() {
|
||||
local root="$1"
|
||||
write_skill "$root/plugins/other-plugin/.apm/skills/cross-plugin-skill" cross-plugin-skill \
|
||||
"Use when doing the other thing. Do not use for anything else."
|
||||
write_skill "$root/plugins/subject-plugin/.apm/skills/my-skill" my-skill \
|
||||
"Use when doing the thing. Do not use for the other thing — use cross-plugin-skill or deployed-only-skill instead."
|
||||
}
|
||||
build_tree "$TMPDIR_T/no-claude"
|
||||
build_tree "$TMPDIR_T/with-claude"
|
||||
# The deployed tree, present only in the second root. Both a skill and an agent,
|
||||
# because both are valid routing targets and both would leak.
|
||||
mkdir -p "$TMPDIR_T/with-claude/.claude/skills/deployed-only-skill" \
|
||||
"$TMPDIR_T/with-claude/.claude/agents"
|
||||
: > "$TMPDIR_T/with-claude/.claude/agents/deployed-only-agent.md"
|
||||
|
||||
run_subject() {
|
||||
local root="$1" out
|
||||
set +e
|
||||
out="$(bash "$HOOK" "$root/plugins/subject-plugin/.apm/skills/my-skill/SKILL.md" 2>&1)"
|
||||
set -e
|
||||
# Normalise the tree root out of the paths so the two runs are comparable.
|
||||
printf '%s\n' "$out" | sed "s#$root#<ROOT>#g"
|
||||
}
|
||||
NO_CLAUDE_OUT="$(run_subject "$TMPDIR_T/no-claude")"
|
||||
WITH_CLAUDE_OUT="$(run_subject "$TMPDIR_T/with-claude")"
|
||||
|
||||
if [[ "$NO_CLAUDE_OUT" == "$WITH_CLAUDE_OUT" ]]; then
|
||||
pass "identical output with and without a deployed .claude/ tree"
|
||||
else
|
||||
fail "the deployed .claude/ tree changed the verdict — without: [$NO_CLAUDE_OUT] with: [$WITH_CLAUDE_OUT]"
|
||||
fi
|
||||
# Identical-but-wrong is still possible (both could resolve everything, or
|
||||
# neither could resolve anything), so the CONTENT is asserted too: the
|
||||
# sibling-plugin name must resolve and the deployed-only name must not.
|
||||
if [[ "$WITH_CLAUDE_OUT" == *"routes to 'deployed-only-skill'"* ]]; then
|
||||
pass "a name that exists only in .claude/ still dangles when an authoring root is present"
|
||||
else
|
||||
fail "the deployed-only target did not dangle — the deployed tree is being read into the universe: $WITH_CLAUDE_OUT"
|
||||
fi
|
||||
if [[ "$WITH_CLAUDE_OUT" != *"routes to 'cross-plugin-skill'"* ]]; then
|
||||
pass "a name in a SIBLING PLUGIN resolves, so the comparison above is not 'nothing resolves'"
|
||||
else
|
||||
fail "the sibling-plugin target dangled — the monorepo universe is not being built: $WITH_CLAUDE_OUT"
|
||||
fi
|
||||
|
||||
# The other half of the rule: with NO authoring root, deployed trees ARE the
|
||||
# universe. That is the consumer case, and without this the rule above could be
|
||||
# implemented as "never read .claude/", which would leave consumers with no
|
||||
# resolution at all.
|
||||
echo ""
|
||||
echo "--- with no authoring root, a deployed .claude/ tree IS the universe ---"
|
||||
CONSUMER="$TMPDIR_T/consumer"
|
||||
mkdir -p "$CONSUMER/.claude/skills/deployed-only-skill"
|
||||
write_skill "$CONSUMER/.claude/skills/my-skill" my-skill \
|
||||
"Use when doing the thing. Do not use for the other thing — use deployed-only-skill instead."
|
||||
set +e
|
||||
CONSUMER_OUT="$(bash "$HOOK" "$CONSUMER/.claude/skills/my-skill/SKILL.md" 2>&1)"
|
||||
CONSUMER_RC=$?
|
||||
set -e
|
||||
if [[ $CONSUMER_RC -eq 0 && "$CONSUMER_OUT" != *"routes to"* && "$CONSUMER_OUT" != *"DID NOT RUN"* ]]; then
|
||||
pass "a sibling in a deployed .claude/skills/ tree resolves when there is no authoring root"
|
||||
else
|
||||
fail "the consumer path did not resolve through the deployed tree (exit $CONSUMER_RC): ${CONSUMER_OUT:-<empty>}"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1a-bis. The consumer case with the one thing every real consumer has: .git
|
||||
# ---------------------------------------------------------------------------
|
||||
# The fixture immediately above has no .git, and that is precisely why it could
|
||||
# never catch this. _authoring_root() falls back to the nearest .git ancestor, so
|
||||
# it returns truthy in ANY git repo — a consumer checkout included. The branch
|
||||
# that reads the deployed trees was guarded by `else`, so in every consumer
|
||||
# checkout the fallback won, _collect_authoring_root() contributed nothing
|
||||
# (there is no plugins/ directory to collect), and _deployed_roots() was dead
|
||||
# code in exactly the case it exists for.
|
||||
#
|
||||
# The pair below is the whole test: the SAME tree, once with .git and once
|
||||
# without. Old behaviour was rc=1 with .git and rc=0 without; a test covering
|
||||
# only the no-.git shape reports green on both.
|
||||
#
|
||||
# `deployed-only-agent` lives ONLY in .agents/agents/, so it can be reached
|
||||
# through no route but _deployed_roots(). `sibling-skill` sits in .claude/skills/
|
||||
# beside the subject, which the sibling-collection block above reaches on its own
|
||||
# — it is the corroborator that makes the dangling target BLOCKING rather than a
|
||||
# SUGGESTION, so the old failure shows up in the exit code and not only in prose.
|
||||
echo ""
|
||||
echo "--- a consumer checkout resolves through its deployed trees even though it is a git repo ---"
|
||||
build_consumer() {
|
||||
local root="$1"
|
||||
mkdir -p "$root/.agents/agents"
|
||||
write_skill "$root/.claude/skills/sibling-skill" sibling-skill \
|
||||
"Use when doing the other thing. Do not use for anything else."
|
||||
write_skill "$root/.claude/skills/my-skill" my-skill \
|
||||
"Use when doing the thing. Do not use for the other thing — use sibling-skill or deployed-only-agent instead."
|
||||
: > "$root/.agents/agents/deployed-only-agent.agent.md"
|
||||
}
|
||||
build_consumer "$TMPDIR_T/consumer-git"
|
||||
mkdir -p "$TMPDIR_T/consumer-git/.git"
|
||||
build_consumer "$TMPDIR_T/consumer-nogit"
|
||||
|
||||
# consumer_case <label> <root>
|
||||
consumer_case() {
|
||||
local label="$1" root="$2" out status=0
|
||||
set +e
|
||||
out="$(bash "$HOOK" "$root/.claude/skills/my-skill/SKILL.md" 2>&1)"
|
||||
status=$?
|
||||
set -e
|
||||
if [[ $status -eq 0 && "$out" != *"routes to"* && "$out" != *"DID NOT RUN"* ]]; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label (exit $status, output: ${out:-<empty>})"
|
||||
fi
|
||||
}
|
||||
consumer_case "an agent in .agents/agents/ resolves in a consumer checkout that HAS a .git directory" \
|
||||
"$TMPDIR_T/consumer-git"
|
||||
consumer_case "control: the same tree without .git resolves too (the shape that always passed)" \
|
||||
"$TMPDIR_T/consumer-nogit"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1a-ter. A monorepo with ONE plugin is still a monorepo
|
||||
# ---------------------------------------------------------------------------
|
||||
# The first attempt at the fix above conditioned the deployed branch on whether
|
||||
# the authoring root had CONTRIBUTED a name — `if len(names) == before:`. That
|
||||
# reads as "the .git fallback collected nothing, so fall through", and it is
|
||||
# wrong: _collect_authoring_root() re-collects the subject's OWN plugin, whose
|
||||
# names the sibling and package blocks have already added. With two plugins
|
||||
# (fixture 1) the cross-plugin name makes the delta non-zero and the guard stays
|
||||
# shut. With ONE plugin the delta is zero, the guard fires in a genuine
|
||||
# monorepo, and _deployed_roots() walks up to ten levels — reaching the user's
|
||||
# global ~/.claude/skills. That is install-dependence again, in the shape
|
||||
# ADR-0020 lines 118-127 exist to forbid.
|
||||
#
|
||||
# So the predicate is which PROBE matched, not how many names arrived. The
|
||||
# assertion is the same shape as fixture 1 — identical verdict either way — but
|
||||
# on a single-plugin tree, which fixture 1 cannot express.
|
||||
echo ""
|
||||
echo "--- a SINGLE-plugin monorepo does not fall through to the deployed trees ---"
|
||||
build_single() {
|
||||
local root="$1"
|
||||
write_skill "$root/plugins/only-plugin/.apm/skills/my-skill" my-skill \
|
||||
"Use when doing the thing. Do not use for the other thing — use /deployed-only-skill instead."
|
||||
}
|
||||
build_single "$TMPDIR_T/single-no-claude"
|
||||
build_single "$TMPDIR_T/single-with-claude"
|
||||
write_skill "$TMPDIR_T/single-with-claude/.claude/skills/deployed-only-skill" deployed-only-skill \
|
||||
"Use when doing the other thing. Do not use for anything else."
|
||||
|
||||
run_single() {
|
||||
local root="$1" out
|
||||
set +e
|
||||
out="$(bash "$HOOK" "$root/plugins/only-plugin/.apm/skills/my-skill/SKILL.md" 2>&1)"
|
||||
set -e
|
||||
printf '%s\n' "$out" | sed "s#$root#<ROOT>#g"
|
||||
}
|
||||
SINGLE_NO_OUT="$(run_single "$TMPDIR_T/single-no-claude")"
|
||||
SINGLE_WITH_OUT="$(run_single "$TMPDIR_T/single-with-claude")"
|
||||
|
||||
if [[ "$SINGLE_NO_OUT" == "$SINGLE_WITH_OUT" ]]; then
|
||||
pass "a single-plugin monorepo gets the same verdict with and without a deployed .claude/ tree"
|
||||
else
|
||||
fail "the deployed tree changed the verdict in a single-plugin monorepo — without: [$SINGLE_NO_OUT] with: [$SINGLE_WITH_OUT]"
|
||||
fi
|
||||
# Identical-but-wrong guard, as in fixture 1: the deployed-only name must DANGLE,
|
||||
# not resolve. Written as `/deployed-only-skill` so it blocks on its own without
|
||||
# needing a second target in the sentence to corroborate it.
|
||||
if [[ "$SINGLE_WITH_OUT" == *"routes to 'deployed-only-skill'"* ]]; then
|
||||
pass "the deployed-only target dangles in a single-plugin monorepo (~/.claude/skills is not in the universe)"
|
||||
else
|
||||
fail "the deployed-only target resolved — the single-plugin tree fell through to _deployed_roots(): $SINGLE_WITH_OUT"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1c. A nested .git inside a plugin must not beat the monorepo root
|
||||
# ---------------------------------------------------------------------------
|
||||
# ADR-0020 records the walk-up as TWO passes — plugins/*/.apm/{skills,agents}
|
||||
# first, .git only afterwards — specifically so a .git inside a plugin (a
|
||||
# submodule, or a sub-package with its own worktree) cannot capture the root.
|
||||
# Nothing anywhere placed a .git inside a plugin, so the second pass was
|
||||
# structural claim only. Collapsing the two probes into one interleaved walk
|
||||
# passes every other fixture in this repo and fails here.
|
||||
echo ""
|
||||
echo "--- a .git INSIDE a plugin does not shadow the monorepo root above it ---"
|
||||
NESTED="$TMPDIR_T/nested-git"
|
||||
write_skill "$NESTED/plugins/other-plugin/.apm/skills/cross-plugin-skill" cross-plugin-skill \
|
||||
"Use when doing the other thing. Do not use for anything else."
|
||||
write_skill "$NESTED/plugins/subject-plugin/.apm/skills/sibling-skill" sibling-skill \
|
||||
"Use when doing the other thing. Do not use for anything else."
|
||||
write_skill "$NESTED/plugins/subject-plugin/.apm/skills/my-skill" my-skill \
|
||||
"Use when doing the thing. Do not use for the other thing — use sibling-skill or cross-plugin-skill instead."
|
||||
# The trap: a git checkout one level BELOW the monorepo root and above the skill.
|
||||
mkdir -p "$NESTED/plugins/subject-plugin/.git"
|
||||
set +e
|
||||
NESTED_OUT="$(bash "$HOOK" "$NESTED/plugins/subject-plugin/.apm/skills/my-skill/SKILL.md" 2>&1)"
|
||||
NESTED_RC=$?
|
||||
set -e
|
||||
# The sibling-plugin name is the discriminator: it is reachable ONLY from the
|
||||
# monorepo root. If the nested .git won, subject-plugin would be the root, its
|
||||
# plugins/ glob would collect nothing, and cross-plugin-skill would dangle —
|
||||
# corroborated by sibling-skill in the same sentence, so it would BLOCK.
|
||||
if [[ $NESTED_RC -eq 0 && "$NESTED_OUT" != *"routes to"* && "$NESTED_OUT" != *"DID NOT RUN"* ]]; then
|
||||
pass "a sibling-plugin target still resolves with a .git directory inside the subject's own plugin"
|
||||
else
|
||||
fail "the nested .git captured the authoring root (exit $NESTED_RC): ${NESTED_OUT:-<empty>}"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1d. Glob metacharacters in the checkout path
|
||||
# ---------------------------------------------------------------------------
|
||||
# The universe is built with glob.glob() against paths that begin with the
|
||||
# checkout directory. A `[`, `]`, `*` or `?` anywhere in that prefix — a worktree
|
||||
# named `feature[2]`, a CI workspace named `build[1]` — turned the literal
|
||||
# directory name into a character class that matched nothing. The resolver then
|
||||
# found no universe at all and degraded to the "DID NOT RUN" INFO with rc=0:
|
||||
# every routing target in the tree silently unchecked, on a gate that reports
|
||||
# green. Same monorepo as above, one directory renamed.
|
||||
echo ""
|
||||
echo "--- glob metacharacters in the checkout path do not silently disable the resolver ---"
|
||||
GLOBDIR="$TMPDIR_T/gl[1]?x/mono"
|
||||
write_skill "$GLOBDIR/plugins/other-plugin/.apm/skills/cross-plugin-skill" cross-plugin-skill \
|
||||
"Use when doing the other thing. Do not use for anything else."
|
||||
write_skill "$GLOBDIR/plugins/subject-plugin/.apm/skills/sibling-skill" sibling-skill \
|
||||
"Use when doing the other thing. Do not use for anything else."
|
||||
write_skill "$GLOBDIR/plugins/subject-plugin/.apm/skills/my-skill" my-skill \
|
||||
"Use when doing the thing. Do not use for the other thing — use sibling-skill or cross-plugin-skill instead."
|
||||
set +e
|
||||
GLOB_OUT="$(bash "$HOOK" "$GLOBDIR/plugins/subject-plugin/.apm/skills/my-skill/SKILL.md" 2>&1)"
|
||||
GLOB_RC=$?
|
||||
set -e
|
||||
if [[ $GLOB_RC -eq 0 && "$GLOB_OUT" != *"DID NOT RUN"* && "$GLOB_OUT" != *"routes to"* ]]; then
|
||||
pass "a monorepo under a directory named 'gl[1]?x' resolves exactly like any other"
|
||||
else
|
||||
fail "glob metacharacters in the path changed the verdict (exit $GLOB_RC): ${GLOB_OUT:-<empty>}"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1b. Machine independence — the real corpus
|
||||
# ---------------------------------------------------------------------------
|
||||
# The fixture above proves the rule; this proves it at the scale where it broke.
|
||||
#
|
||||
# The A/B is built rather than borrowed. plugins/ is copied TWICE: once bare (a
|
||||
# fresh clone), and once with a synthetic .claude/skills/ tree deployed beside it
|
||||
# holding a directory for every name the corpus currently reports as dangling. If
|
||||
# deployed trees leaked back into the universe, the second copy would resolve
|
||||
# those names and report an empty dangling set while the first reported two —
|
||||
# 2-vs-6, reproduced deterministically.
|
||||
#
|
||||
# Deliberately NOT keyed on whether THIS machine has run `apm install`. Doing that
|
||||
# would make the suite fail on a fresh clone (where there is no .claude/ to
|
||||
# contrast against) — a test of machine independence that is itself
|
||||
# machine-dependent. The live tree is still compared, as a third data point, but
|
||||
# nothing here requires it to be in either state.
|
||||
echo ""
|
||||
echo "--- the real corpus reports the same dangling targets with and without a deployed tree ---"
|
||||
dangling_set() {
|
||||
local -a files=()
|
||||
local f
|
||||
# Collected with a `while read` loop, not `mapfile`: macOS ships bash 3.2,
|
||||
# which has no `mapfile`, and tests/test-vale-wrap.sh scans tests/*.sh for
|
||||
# exactly that hazard. `find` rather than a glob so both roots walk identically.
|
||||
while IFS= read -r f; do
|
||||
files+=("$f")
|
||||
done < <(find "$1" -path '*/.apm/skills/*/SKILL.md' | sort)
|
||||
if [[ ${#files[@]} -eq 0 ]]; then
|
||||
echo "NO-FILES-FOUND"
|
||||
return
|
||||
fi
|
||||
set +e
|
||||
bash "$HOOK" ${files[@]+"${files[@]}"} 2>&1 \
|
||||
| grep -oE "routes to '[^']+'" \
|
||||
| sed "s/routes to '//; s/'//" \
|
||||
| sort -u
|
||||
set -e
|
||||
}
|
||||
LIVE_DANGLING="$(dangling_set "$REPO_ROOT/plugins")"
|
||||
|
||||
FRESH_ROOT="$TMPDIR_T/fresh-clone"
|
||||
mkdir -p "$FRESH_ROOT"
|
||||
cp -R "$REPO_ROOT/plugins" "$FRESH_ROOT/plugins"
|
||||
[[ -f "$REPO_ROOT/apm.yml" ]] && cp "$REPO_ROOT/apm.yml" "$FRESH_ROOT/apm.yml"
|
||||
FRESH_DANGLING="$(dangling_set "$FRESH_ROOT/plugins")"
|
||||
|
||||
DEPLOYED_ROOT="$TMPDIR_T/deployed-clone"
|
||||
mkdir -p "$DEPLOYED_ROOT/.claude/skills" "$DEPLOYED_ROOT/.claude/agents"
|
||||
cp -R "$REPO_ROOT/plugins" "$DEPLOYED_ROOT/plugins"
|
||||
[[ -f "$REPO_ROOT/apm.yml" ]] && cp "$REPO_ROOT/apm.yml" "$DEPLOYED_ROOT/apm.yml"
|
||||
# Deploy exactly the names that currently dangle. That is the strongest possible
|
||||
# bait: if the deployed tree were consulted, every one of them would resolve and
|
||||
# the dangling set would collapse to empty.
|
||||
DEPLOY_COUNT=0
|
||||
while IFS= read -r name; do
|
||||
[[ -n "$name" ]] || continue
|
||||
mkdir -p "$DEPLOYED_ROOT/.claude/skills/$name"
|
||||
DEPLOY_COUNT=$((DEPLOY_COUNT + 1))
|
||||
done <<< "$FRESH_DANGLING"
|
||||
DEPLOYED_DANGLING="$(dangling_set "$DEPLOYED_ROOT/plugins")"
|
||||
|
||||
if [[ "$DEPLOY_COUNT" -gt 0 ]]; then
|
||||
pass "precondition: $DEPLOY_COUNT dangling name(s) deployed into the contrast tree's .claude/skills/, so the A/B has something to distinguish"
|
||||
else
|
||||
fail "no dangling names to deploy — the corpus reports none, so this A/B distinguishes nothing. Deploy a known-absent name explicitly instead of deriving one."
|
||||
fi
|
||||
if [[ ! -d "$FRESH_ROOT/.claude" && ! -d "$FRESH_ROOT/.agents" ]]; then
|
||||
pass "precondition: the fresh-clone copy has no deployed tree of its own"
|
||||
else
|
||||
fail "the fresh-clone copy picked up a deployed tree — it is not a fresh-clone fixture"
|
||||
fi
|
||||
if [[ "$FRESH_DANGLING" == "$DEPLOYED_DANGLING" ]]; then
|
||||
pass "deploying every dangling name into .claude/skills/ changes nothing: $(echo "$FRESH_DANGLING" | tr '\n' ' ')"
|
||||
else
|
||||
fail "the corpus verdict depends on whether apm install has been run — fresh clone: [$(echo "$FRESH_DANGLING" | tr '\n' ' ')] with a deployed tree: [$(echo "$DEPLOYED_DANGLING" | tr '\n' ' ')]"
|
||||
fi
|
||||
# Third data point: whatever state THIS machine happens to be in, the live tree
|
||||
# must agree with a bare copy of the same plugins/. No precondition on that state
|
||||
# — see the section header.
|
||||
if [[ "$LIVE_DANGLING" == "$FRESH_DANGLING" ]]; then
|
||||
pass "the live tree agrees with a bare copy (this machine $( [[ -d "$REPO_ROOT/.claude/skills" ]] && echo "HAS" || echo "has no" ) deployed .claude/skills/ tree)"
|
||||
else
|
||||
fail "the live tree disagrees with a bare copy of the same plugins/ — live: [$(echo "$LIVE_DANGLING" | tr '\n' ' ')] fresh clone: [$(echo "$FRESH_DANGLING" | tr '\n' ' ')]"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1c. The two live true positives, pinned by name
|
||||
# ---------------------------------------------------------------------------
|
||||
# ADR-0020 records these as real broken routing targets and splits fixing them
|
||||
# into Gitea issue #100. Until that lands they are the ONLY evidence the dangling
|
||||
# check finds anything at all in real prose, so they are asserted as an exact set
|
||||
# rather than a "contains" — a false-positive fix that suppressed one of them
|
||||
# would otherwise land green.
|
||||
#
|
||||
# `gitea-labels` is the subtler of the two and is worth keeping: it is not
|
||||
# written anywhere as `gitea-labels`. gitea-issues' description says "Composes
|
||||
# `gitea-labels-\n milestones`" in a `>`-folded scalar, and the fold joins the
|
||||
# lines into "gitea-labels- milestones" — the trailing hyphen is what keeps the
|
||||
# token terminal and therefore danglable.
|
||||
#
|
||||
# WHEN ISSUE #100 IS FIXED: update EXPECTED_DANGLING to match. Do not delete the
|
||||
# assertion — an empty expected set is fine and still pins that no NEW dangling
|
||||
# target appeared.
|
||||
echo ""
|
||||
echo "--- the two live dangling targets in the corpus are exactly the two ADR-0020 records ---"
|
||||
EXPECTED_DANGLING="$(printf '%s\n' gitea-labels neuledge-context)"
|
||||
if [[ "$LIVE_DANGLING" == "$EXPECTED_DANGLING" ]]; then
|
||||
pass "the corpus dangling set is exactly {gitea-labels, neuledge-context}"
|
||||
else
|
||||
fail "the corpus dangling set changed — expected [$(echo "$EXPECTED_DANGLING" | tr '\n' ' ')], got [$(echo "$LIVE_DANGLING" | tr '\n' ' ')]. If a retrofit fixed one, update EXPECTED_DANGLING; if a false-positive fix silently deleted one, that is the regression this asserts."
|
||||
fi
|
||||
for probe in \
|
||||
"plugins/bin/.apm/skills/research/SKILL.md:neuledge-context" \
|
||||
"plugins/gitea/.apm/skills/gitea-issues/SKILL.md:gitea-labels"; do
|
||||
probe_file="$REPO_ROOT/${probe%%:*}"
|
||||
probe_name="${probe##*:}"
|
||||
if [[ ! -f "$probe_file" ]]; then
|
||||
fail "the true-positive fixture ${probe%%:*} no longer exists — this pin has become vacuous"
|
||||
continue
|
||||
fi
|
||||
set +e
|
||||
probe_out="$(bash "$HOOK" "$probe_file" 2>&1)"
|
||||
set -e
|
||||
if [[ "$probe_out" == *"routes to '$probe_name'"* ]]; then
|
||||
pass "detects the dangling '$probe_name' target in ${probe%%:*}"
|
||||
else
|
||||
fail "did not detect the dangling '$probe_name' target in ${probe%%:*} — a false-positive fix has taken a true positive with it: $probe_out"
|
||||
fi
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. The bare-target grammar rule
|
||||
# ---------------------------------------------------------------------------
|
||||
# Every fixture is built inside a real plugin tree. In a bare temp directory the
|
||||
# resolver would decline ("DID NOT RUN") and every must-not-error case would pass
|
||||
# vacuously, proving nothing about extraction.
|
||||
echo ""
|
||||
echo "--- attributive compound modifiers are prose, not routing targets ---"
|
||||
GRAMMAR_ROOT="$TMPDIR_T/grammar"
|
||||
write_skill "$GRAMMAR_ROOT/plugins/p/.apm/skills/sibling-skill" sibling-skill \
|
||||
"Use when doing the other thing. Do not use for anything else."
|
||||
|
||||
# grammar_case <slug> <expect: silent|errors> <needle> <description>
|
||||
grammar_case() {
|
||||
local slug="$1" mode="$2" needle="$3" desc="$4" out status=0
|
||||
write_skill "$GRAMMAR_ROOT/plugins/p/.apm/skills/$slug" "$slug" "$desc"
|
||||
set +e
|
||||
out="$(bash "$HOOK" "$GRAMMAR_ROOT/plugins/p/.apm/skills/$slug/SKILL.md" 2>&1)"
|
||||
status=$?
|
||||
set -e
|
||||
if [[ "$out" == *"DID NOT RUN"* ]]; then
|
||||
fail "\"$desc\" — the resolver declined, so this case asserts nothing about extraction: $out"
|
||||
return
|
||||
fi
|
||||
case "$mode" in
|
||||
silent)
|
||||
if [[ $status -eq 0 && -z "$out" ]]; then
|
||||
pass "not a dangling target: \"$desc\""
|
||||
else
|
||||
fail "\"$desc\" (exit $status, output: ${out:-<empty>})"
|
||||
fi
|
||||
;;
|
||||
errors)
|
||||
if [[ $status -ne 0 && "$out" == *"$needle"* ]]; then
|
||||
pass "still a dangling target: \"$desc\""
|
||||
else
|
||||
fail "\"$desc\" should have ERRORed with $needle (exit $status, output: ${out:-<empty>})"
|
||||
fi
|
||||
;;
|
||||
suggests)
|
||||
# Reported, not blocking. Both halves matter: an ERROR here would be the
|
||||
# unsuppressable false positive this tier exists to remove, and silence
|
||||
# would mean the gate stopped noticing the target at all.
|
||||
if [[ $status -eq 0 && "$out" == *"SUGGESTION"*"$needle"* && "$out" != *"ERROR"* ]]; then
|
||||
pass "reported but not blocking: \"$desc\""
|
||||
else
|
||||
fail "\"$desc\" should have exited 0 with a SUGGESTION naming $needle (exit $status, output: ${out:-<empty>})"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# The four phrasings that were hard dangling FAILs with no suppression. All four
|
||||
# are lifted from real descriptions in this corpus.
|
||||
grammar_case fp-precommit-hooks silent "" \
|
||||
"Use when running the linter. Use pre-commit hooks instead of ad-hoc scripts."
|
||||
grammar_case fp-pull-request silent "" \
|
||||
"Use when opening changes. Invoke the pull-request template instead of writing one by hand."
|
||||
grammar_case fp-conventional silent "" \
|
||||
"Use when writing history. Use conventional-commits formatting rather than free-form messages."
|
||||
grammar_case fp-prepush-backticked silent "" \
|
||||
"Use when checking a branch. Do not use for local edits — run the \`pre-push\` hooks instead."
|
||||
|
||||
echo ""
|
||||
echo "--- a lone unresolvable token in terminal position is REPORTED, not blocking ---"
|
||||
# The class this tier was added for. Every one of these is grammatically
|
||||
# identical to a real broken route — "route verb + name + terminal" is also how
|
||||
# prose cites a hook, a linter, a file format or an English compound — and every
|
||||
# one of them was a hard FAIL with no suppression mechanism anywhere in the gate.
|
||||
# The skills most exposed are exactly the ones the ADR-0020 retrofit sends
|
||||
# authors back to rewrite first: pc-run, pc-author, vale-run, vale-config and the
|
||||
# apm-* family are all ABOUT hyphenated tools.
|
||||
grammar_case fp-precommit-terminal suggests "routes to 'pre-commit'" \
|
||||
"Use when running the linter. Do not use for running hooks — run \`pre-commit\` instead."
|
||||
grammar_case fp-commit-msg suggests "routes to 'commit-msg'" \
|
||||
"Use when writing history. Do not use for the commit message — see \`commit-msg\`."
|
||||
grammar_case fp-type-check suggests "routes to 'type-check'" \
|
||||
"Use when compiling. Do not use for type errors — run \`type-check\` first."
|
||||
grammar_case fp-semantic-release suggests "routes to 'semantic-release'" \
|
||||
"Use when tagging a version. Instead, use \`semantic-release\`."
|
||||
# Single-word tool names are the same defect: `eslint` in terminal position hit
|
||||
# the marked-target path and hard-FAILed just as `pre-commit` did.
|
||||
grammar_case fp-eslint suggests "routes to 'eslint'" \
|
||||
"Use when linting JS. Do not use for style — run \`eslint\` instead."
|
||||
# Bare English compounds, which the backtick path never sees at all.
|
||||
grammar_case fp-clean-up suggests "routes to 'clean-up'" \
|
||||
"Use when doing the thing. Do not use for the old flow — use the clean-up instead."
|
||||
grammar_case fp-built-in suggests "routes to 'built-in'" \
|
||||
"Use when doing the thing. Do not use for the custom path — Instead, prefer the built-in."
|
||||
grammar_case fp-write-up suggests "routes to 'write-up'" \
|
||||
"Use when doing the thing. Do not use for the summary — see the write-up."
|
||||
grammar_case fp-front-end suggests "routes to 'front-end'" \
|
||||
"Use when doing the thing. Do not use for the API layer — use the front-end."
|
||||
grammar_case fp-unit-tests suggests "routes to 'unit-tests'" \
|
||||
"Use when testing. Do not run end-to-end, run unit-tests."
|
||||
|
||||
echo ""
|
||||
echo "--- terminal targets that resolve to nothing still ERROR ---"
|
||||
# The controls. Without them the cases above are satisfied by a check that never
|
||||
# fires, and the narrowing would have eaten the gate rather than sharpened it.
|
||||
# Three forms, three code paths:
|
||||
# * route NOTATION — `/name` and `-> name` — is exempt from corroboration and
|
||||
# blocks on its own. Nobody writes `/pre-commit` or `-> pre-commit` to mean
|
||||
# the hook, so there is no ambiguity to resolve, and an author who wants a
|
||||
# route checked unconditionally has two ways to say so.
|
||||
# * a PROSE-form target — backticked or bare — blocks when its own sentence
|
||||
# names another target that resolves. `sibling-skill` is that corroborator
|
||||
# here; it is the same shape as both live true positives, which sit beside
|
||||
# `write-docs` and `gitea-labels-milestones` respectively.
|
||||
grammar_case tp-arrow errors "routes to 'no-such-arrow-target'" \
|
||||
"Use when doing the thing. Not the other thing → no-such-arrow-target."
|
||||
grammar_case tp-slash errors "routes to 'no-such-slash-skill'" \
|
||||
"Use when doing the thing. Do not use for improvements — use /no-such-slash-skill instead."
|
||||
grammar_case tp-backticked errors "routes to 'no-such-backticked-skill'" \
|
||||
"Use when doing the thing. Do not use for improvements — use \`sibling-skill\` or \`no-such-backticked-skill\` instead."
|
||||
grammar_case tp-bare-terminal errors "routes to 'no-such-bare-skill'" \
|
||||
"Use when doing the thing. Do not use for improvements — use sibling-skill or no-such-bare-skill instead."
|
||||
|
||||
echo ""
|
||||
echo "--- corroboration is scoped to a REAL sentence, not to whatever the splitter says ---"
|
||||
# Corroboration decides SUGGESTION vs blocking ERROR, so a mis-placed sentence
|
||||
# boundary moves a target between the two tiers. The naive "period, space,
|
||||
# capital" rule got this wrong in both directions, and both were live:
|
||||
#
|
||||
# OVER-SPLIT. `e.g. "..."` is not a sentence end, but the quote looks like a
|
||||
# start. The clause was cut in half and the corroborator stranded on the far
|
||||
# side, so a target that DOES sit beside a resolving sibling silently demoted
|
||||
# to SUGGESTION — a measurement taken and then discarded.
|
||||
grammar_case abbrev-split errors "routes to 'no-such-abbrev-skill'" \
|
||||
"Use when doing the thing. Do not use for improvements — use sibling-skill first, e.g. \"run the audit\", then use no-such-abbrev-skill instead."
|
||||
#
|
||||
# UNDER-SPLIT. A sentence opening with a lowercase word or a code span was not
|
||||
# seen as a start at all, so two sentences merged and a resolving target in the
|
||||
# FIRST vouched for an unresolvable one in the SECOND that it never stood
|
||||
# beside — a hard FAIL with no escape hatch, which is the exact failure
|
||||
# corroboration was added to prevent. The target must still be REPORTED; only
|
||||
# the power to block is withdrawn.
|
||||
grammar_case lowercase-start suggests "routes to 'no-such-lower-skill'" \
|
||||
"Use when doing the thing. Use sibling-skill for the main case. do not use for improvements — use no-such-lower-skill instead."
|
||||
grammar_case backtick-start suggests "routes to 'no-such-tick-skill'" \
|
||||
"Use when doing the thing. Use sibling-skill for the main case. \`no-such-tick-skill\` is not for this — do not use it instead."
|
||||
|
||||
# And the confirming half of the grammar rule: a compound-modifier target is
|
||||
# CONFIRM-ONLY, not ignored. When the name does exist it still counts as a route
|
||||
# — the rule suppresses the ERROR, it does not delete the target.
|
||||
echo ""
|
||||
echo "--- an attributive target that DOES resolve is still a route, not a discarded token ---"
|
||||
write_skill "$GRAMMAR_ROOT/plugins/p/.apm/skills/attributive-subject" attributive-subject \
|
||||
"Use when doing the thing. Do not use for the other thing — use the sibling-skill helper instead."
|
||||
set +e
|
||||
ATTR_OUT="$(bash "$HOOK" "$GRAMMAR_ROOT/plugins/p/.apm/skills/attributive-subject/SKILL.md" 2>&1)"
|
||||
ATTR_RC=$?
|
||||
set -e
|
||||
if [[ $ATTR_RC -eq 0 && -z "$ATTR_OUT" ]]; then
|
||||
pass "a resolving attributive target neither errors nor is reported"
|
||||
else
|
||||
fail "an attributive target naming a REAL skill produced output (exit $ATTR_RC): $ATTR_OUT"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
@@ -457,8 +457,8 @@ fi
|
||||
|
||||
# --- 9b. Exits 1 when a per-rule override leaves a rule at anything but error ---
|
||||
# The third way to switch a rule off without touching a style file or a glob.
|
||||
# CONTEXT.md's "Vale audit prefilter" entry: "Every rule is `level: error` and
|
||||
# every alert is a FAIL -- no ignorable tier". Vale's exit code keys on `error`
|
||||
# Per ADR-0013, every rule is `level: error` and every alert is a FAIL -- there
|
||||
# is no ignorable tier. Vale's exit code keys on `error`
|
||||
# alerts alone, so any such override leaves the glob intact, the styles
|
||||
# byte-identical, and the run at `0 errors`, exit 0, `Passed`.
|
||||
#
|
||||
@@ -553,9 +553,9 @@ fi
|
||||
# equality check applies, and case 10's probe still passed because it keys on a
|
||||
# Kyberforge alert. Verified dead by probing a `.agent.md` carrying
|
||||
# "Use proactively": 0 alerts under the broken config, KyberforgeCopilot.
|
||||
# ProactivePhrase under the shipped one. CONTEXT.md describes the style as
|
||||
# "scoped only to `.agent.md` files for the Copilot-only 'Use proactively has
|
||||
# no effect' check", so shipping it unloaded is drift.
|
||||
# ProactivePhrase under the shipped one. ADR-0013 scopes the style to
|
||||
# `.agent.md` files only, for the Copilot-only 'Use proactively has no effect'
|
||||
# check, so shipping it unloaded is drift.
|
||||
echo ""
|
||||
echo "--- exits 1 when the shipped KyberforgeCopilot style is named by no BasedOnStyles ---"
|
||||
FIXTURE11C="$(make_fixture)"
|
||||
@@ -609,9 +609,9 @@ fi
|
||||
# still matched all of them and the check passed -- while a project-scope
|
||||
# `.claude/skills/foo/SKILL.md` started linting as `0 errors ... in 0 files`,
|
||||
# exit 0, hook `Passed`: the exact failure the script's own header comment says
|
||||
# it exists to catch. CONTEXT.md: "A `SKILL.md` outside `plugins/` (e.g.
|
||||
# project-scope `.claude/skills/foo/SKILL.md`) still matches `[**/SKILL.md]` and
|
||||
# gets linted normally -- the globs constrain filename shape, not location."
|
||||
# it exists to catch. A `SKILL.md` outside `plugins/` (e.g. project-scope
|
||||
# `.claude/skills/foo/SKILL.md`) still matches `[**/SKILL.md]` and gets linted
|
||||
# normally -- the globs constrain filename shape, not location.
|
||||
# These narrowings are still valid glob syntax and break no `plugins/`-shaped
|
||||
# file, so only a non-`plugins/` probe path catches them.
|
||||
echo ""
|
||||
|
||||
+77
-11
@@ -392,10 +392,11 @@ fi
|
||||
|
||||
# --- 10. --strict turns a skip into a failure, and names the suite AND the reason ---
|
||||
# Graceful skipping is right for an ad-hoc run and wrong for a gate. At pre-push a
|
||||
# suite exiting 77 means a dependency AGENTS.md documents as required is missing
|
||||
# on the pushing machine -- and pre-commit prints nothing at all for a passing
|
||||
# hook, so the skip list this script writes to stdout was swallowed whole. A
|
||||
# vale-less PATH shipped a green gate having verified 15 of 17 suites.
|
||||
# suite exiting 77 means a dependency README.md's Prerequisites table documents
|
||||
# as required is missing on the pushing machine -- and pre-commit prints nothing
|
||||
# at all for a passing hook, so the skip list this script writes to stdout was
|
||||
# swallowed whole. A vale-less PATH once shipped a green gate having verified
|
||||
# 15 of the 17 suites that existed then.
|
||||
#
|
||||
# The reason is asserted, not just the name: "something was skipped" leaves the
|
||||
# reader with no idea which binary to install, which is most of why the swallowed
|
||||
@@ -538,13 +539,24 @@ else
|
||||
fi
|
||||
|
||||
# --- 10g. An ambient RUN_TESTS_STRICT=1 must not reach a fixture that did not ask
|
||||
# for it. This suite is discovered and run by run-tests.sh itself, so under the
|
||||
# gate the variable is exported into every child here. That is not a hypothetical:
|
||||
# `bash tests/run-tests.sh` reported 19 passed while
|
||||
# `RUN_TESTS_STRICT=1 bash tests/run-tests.sh` reported this file as the one
|
||||
# failure, because case 10c's deliberately-non-strict run inherited strict and
|
||||
# went red. The gate was therefore RED for everyone, and the only way to see it
|
||||
# was to run the gate.
|
||||
# for it. This suite is discovered and run by run-tests.sh itself, so when the
|
||||
# outer run is launched with the env-var spelling the variable used to be exported
|
||||
# into every child here. That was not a hypothetical: `bash tests/run-tests.sh`
|
||||
# was green while `RUN_TESTS_STRICT=1 bash tests/run-tests.sh` reported this file
|
||||
# as the one failure, because case 10c's deliberately-non-strict run inherited
|
||||
# strict and went red.
|
||||
#
|
||||
# Two corrections to the record, because both were overstated before:
|
||||
# * The blast radius was TWO assertions, not six -- cases 10c and 10g here, and
|
||||
# nothing else in the repo reads RUN_TESTS_STRICT.
|
||||
# * The pre-push GATE was never red. It runs `bash tests/run-tests.sh --strict`
|
||||
# (see .pre-commit-config.yaml), and the flag sets a shell local that is never
|
||||
# exported, so the flag spelling never leaked. Only the env-var spelling did.
|
||||
#
|
||||
# run-tests.sh now `unset`s the variable immediately after latching it, so the
|
||||
# leak is closed at its source and the two spellings hand children an identical
|
||||
# environment (case 10i pins that directly). The `env -u` in run_fake() is kept as
|
||||
# this suite's own defence-in-depth rather than as the fix.
|
||||
#
|
||||
# The variable is exported here rather than passed as a prefix on purpose: a
|
||||
# prefix (`RUN_TESTS_STRICT=1 run_fake ...`) applies to the function call, and
|
||||
@@ -604,6 +616,60 @@ else
|
||||
pass "--strict reports skipped suites on stderr and suppresses the duplicate stdout list"
|
||||
fi
|
||||
|
||||
# --- 10i. The two documented invocations are equivalent in what a CHILD sees ---
|
||||
# `bash tests/run-tests.sh --strict` and `RUN_TESTS_STRICT=1 bash
|
||||
# tests/run-tests.sh` are documented as the same switch, and case 10b already
|
||||
# asserts they produce the same PARENT verdict. That is the weaker half: the two
|
||||
# differed in the ENVIRONMENT they handed every dispatched test-*.sh, because the
|
||||
# flag sets a shell local while the env var stayed exported down the whole process
|
||||
# tree. A dispatched suite could therefore behave differently depending on which
|
||||
# spelling launched the run above it -- which is how case 10c went red under one
|
||||
# invocation and green under the other.
|
||||
#
|
||||
# So this asks the children directly rather than reading the parent's summary. The
|
||||
# case script reports whether RUN_TESTS_STRICT is present in its own environment
|
||||
# AT ALL (`${VAR+set}`, not `${VAR:-}` -- an exported empty value is still a leak),
|
||||
# and both spellings must report it absent. Equivalence is asserted between the two
|
||||
# observations, not just against a hardcoded expectation, so the two cannot drift
|
||||
# apart in some future direction neither case anticipated.
|
||||
echo ""
|
||||
echo "--- --strict and RUN_TESTS_STRICT=1 hand children the same environment ---"
|
||||
DIR10I="$(make_fake_repo)"
|
||||
FIXTURES+=("$DIR10I")
|
||||
install_healthy_bats_runner "$DIR10I"
|
||||
add_case "$DIR10I" test-reports-its-env.sh <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
if [[ -n "${RUN_TESTS_STRICT+set}" ]]; then
|
||||
echo "CHILD-SAW-STRICT=[${RUN_TESTS_STRICT}]"
|
||||
else
|
||||
echo "CHILD-SAW-STRICT=<absent>"
|
||||
fi
|
||||
EOF
|
||||
# Flag spelling: run_fake scrubs the ambient variable first, so what the child
|
||||
# sees here is purely a function of what run-tests.sh itself exports.
|
||||
run_fake "$DIR10I" --strict
|
||||
FLAG_CHILD="$(echo "$FAKE_OUT" | grep -o 'CHILD-SAW-STRICT=.*' | head -n 1 || true)"
|
||||
FLAG_RC=$FAKE_RC
|
||||
# Env spelling: invoked directly, NOT through run_fake, because run_fake's `env -u`
|
||||
# would strip the very variable under test.
|
||||
I_PRIV="$(mktemp -d)"
|
||||
FIXTURES+=("$I_PRIV")
|
||||
ENV_RC=0
|
||||
ENV_OUT="$(TMPDIR="$I_PRIV" TEST_DIR="$DIR10I/cases" RUN_TESTS_STRICT=1 \
|
||||
bash "$DIR10I/tests/run-tests.sh" 2>&1)" || ENV_RC=$?
|
||||
ENV_CHILD="$(echo "$ENV_OUT" | grep -o 'CHILD-SAW-STRICT=.*' | head -n 1 || true)"
|
||||
if [[ -z "$FLAG_CHILD" || -z "$ENV_CHILD" ]]; then
|
||||
fail "the reporting case script never ran under one of the two invocations (flag: '${FLAG_CHILD:-<none>}', env: '${ENV_CHILD:-<none>}')"
|
||||
elif [[ "$FLAG_CHILD" != "$ENV_CHILD" ]]; then
|
||||
fail "the two documented invocations hand children different environments — flag: $FLAG_CHILD, env: $ENV_CHILD"
|
||||
elif [[ "$ENV_CHILD" != "CHILD-SAW-STRICT=<absent>" ]]; then
|
||||
fail "RUN_TESTS_STRICT is still exported to dispatched suites ($ENV_CHILD) — a suite that itself runs run-tests.sh inherits strictness it never asked for"
|
||||
elif [[ $FLAG_RC -ne 0 || $ENV_RC -ne 0 ]]; then
|
||||
fail "a clean fixture failed under one of the two invocations (flag rc=$FLAG_RC, env rc=$ENV_RC): $FAKE_OUT / $ENV_OUT"
|
||||
else
|
||||
pass "--strict and RUN_TESTS_STRICT=1 both dispatch children with RUN_TESTS_STRICT absent"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
+156
-37
@@ -294,6 +294,61 @@ make_budget_fixture() {
|
||||
echo "$file"
|
||||
}
|
||||
|
||||
# desc_of_length <n> — a description of EXACTLY n characters that carries a
|
||||
# boundary clause and names no routing target.
|
||||
#
|
||||
# ADR-0020's missing-boundary-clause SUGGESTION fires on every description
|
||||
# without one, so a fixture that omits it is never "otherwise clean": a test
|
||||
# asserting silence would be asserting the boundary check's ABSENCE rather than
|
||||
# the length boundary it names. The clause is paid for out of the same budget
|
||||
# being measured (padding arithmetic, not a fixed suffix) so the character count
|
||||
# stays exact. "anything else" is unhyphenated, so no routing target rides along.
|
||||
desc_of_length() {
|
||||
python3 - "$1" <<'PY'
|
||||
import sys
|
||||
n = int(sys.argv[1])
|
||||
prefix = 'Use when doing the thing. Do not use for anything else. '
|
||||
assert n >= len(prefix), 'requested description shorter than the boundary clause'
|
||||
print(prefix + 'x' * (n - len(prefix)))
|
||||
PY
|
||||
}
|
||||
|
||||
# make_tree_fixture <label> <desc> <body_words> — a SKILL.md inside a synthetic
|
||||
# apm plugin monorepo, so the boundary-target resolver has a universe.
|
||||
#
|
||||
# Resolution walks up FROM THE TARGET FILE to an authoring root (the nearest
|
||||
# ancestor holding plugins/*/.apm/{skills,agents}, falling back to .git); it is
|
||||
# never derived from the checker's own location, because deriving it from
|
||||
# ${BASH_SOURCE} leaked this repo's 39-skill universe into every consumer repo
|
||||
# running the hook. A fixture in a bare mktemp -d therefore has NO universe and
|
||||
# correctly reports "DID NOT RUN" — that is not a bug to paper over with a
|
||||
# looser assertion, it is why the fixture has to be a real tree:
|
||||
#
|
||||
# <root>/plugins/subject-plugin/.apm/skills/<label>/SKILL.md <- the subject
|
||||
# <root>/plugins/subject-plugin/.apm/skills/sibling-skill/ <- same package
|
||||
# <root>/plugins/subject-plugin/.apm/agents/sibling-agent.agent.md
|
||||
# <root>/plugins/other-plugin/.apm/skills/cross-plugin-skill/ <- sibling plugin
|
||||
#
|
||||
# The sibling plugin is what makes "every plugin in the monorepo contributes its
|
||||
# names" testable; without it a cross-plugin target and a typo are the same.
|
||||
make_tree_fixture() {
|
||||
local label="$1" desc="$2" body_words="$3" root apm
|
||||
root="$TMPDIR/tree-$label"
|
||||
apm="$root/plugins/subject-plugin/.apm"
|
||||
mkdir -p "$apm/skills/$label" "$apm/skills/sibling-skill" "$apm/agents" \
|
||||
"$root/plugins/other-plugin/.apm/skills/cross-plugin-skill"
|
||||
: > "$apm/agents/sibling-agent.agent.md"
|
||||
{
|
||||
echo "---"
|
||||
echo "name: $label"
|
||||
echo "description: $desc"
|
||||
echo "---"
|
||||
echo ""
|
||||
python3 -c "print(' '.join(['word'] * $body_words))"
|
||||
} > "$apm/skills/$label/SKILL.md"
|
||||
echo "$apm/skills/$label/SKILL.md"
|
||||
}
|
||||
|
||||
# expect_gate <label> <expected: pass|suggest|fail> <file> [needle]
|
||||
expect_gate() {
|
||||
local label="$1" expected="$2" file="$3" needle="${4:-}" out status
|
||||
@@ -323,15 +378,26 @@ expect_gate() {
|
||||
fail "$label (exit $status, output: ${out:-<empty>})"
|
||||
fi
|
||||
;;
|
||||
# A check that DECLINED to run must say so and must not fail the file. The
|
||||
# ERROR guard is the point: a declined check that also errored would satisfy
|
||||
# a bare "output contains INFO" assertion.
|
||||
info)
|
||||
if [[ $status -eq 0 && "$out" == *"INFO"* && "$out" == *"$needle"* \
|
||||
&& "$out" != *"ERROR"* ]]; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label (exit $status, output: ${out:-<empty>})"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "--- description budget: $DESC_SUGGEST_CHARS SUGGESTION / $DESC_MAX_CHARS FAIL, both inclusive ---"
|
||||
D_AT_SUGGEST="$(python3 -c "print('x' * $DESC_SUGGEST_CHARS)")"
|
||||
D_OVER_SUGGEST="$(python3 -c "print('x' * $((DESC_SUGGEST_CHARS + 1)))")"
|
||||
D_AT_MAX="$(python3 -c "print('x' * $DESC_MAX_CHARS)")"
|
||||
D_OVER_MAX="$(python3 -c "print('x' * $((DESC_MAX_CHARS + 1)))")"
|
||||
D_AT_SUGGEST="$(desc_of_length "$DESC_SUGGEST_CHARS")"
|
||||
D_OVER_SUGGEST="$(desc_of_length "$((DESC_SUGGEST_CHARS + 1))")"
|
||||
D_AT_MAX="$(desc_of_length "$DESC_MAX_CHARS")"
|
||||
D_OVER_MAX="$(desc_of_length "$((DESC_MAX_CHARS + 1))")"
|
||||
expect_gate "description at exactly $DESC_SUGGEST_CHARS chars is silent" \
|
||||
pass "$(make_budget_fixture desc-at-suggest "$D_AT_SUGGEST" 10)"
|
||||
expect_gate "description at $((DESC_SUGGEST_CHARS + 1)) chars suggests and exits 0" \
|
||||
@@ -359,18 +425,23 @@ FOLDED="$TMPDIR/folded.md"
|
||||
expect_gate "a >-folded 450-char description fails (raw first line would read as 1 char)" \
|
||||
fail "$FOLDED" "description is 450 characters"
|
||||
|
||||
# Every body fixture below carries a boundary clause for the same reason
|
||||
# desc_of_length() does: without one the missing-boundary-clause SUGGESTION
|
||||
# fires and a body-budget test that asserts silence stops isolating the body
|
||||
# budget. It is short, so the description gate stays quiet too.
|
||||
CLEAN_DESC="Short valid description. Do not use for anything else."
|
||||
echo ""
|
||||
echo "--- body budget: $BODY_SUGGEST_WORDS SUGGESTION / $BODY_MAX_WORDS FAIL, body only, both inclusive ---"
|
||||
expect_gate "body at exactly $BODY_SUGGEST_WORDS words is silent" \
|
||||
pass "$(make_budget_fixture body-at-suggest "Short valid description." "$BODY_SUGGEST_WORDS")"
|
||||
pass "$(make_budget_fixture body-at-suggest "$CLEAN_DESC" "$BODY_SUGGEST_WORDS")"
|
||||
expect_gate "body at $((BODY_SUGGEST_WORDS + 1)) words suggests and exits 0" \
|
||||
suggest "$(make_budget_fixture body-over-suggest "Short valid description." "$((BODY_SUGGEST_WORDS + 1))")" \
|
||||
suggest "$(make_budget_fixture body-over-suggest "$CLEAN_DESC" "$((BODY_SUGGEST_WORDS + 1))")" \
|
||||
"body is $((BODY_SUGGEST_WORDS + 1)) words"
|
||||
expect_gate "body at exactly $BODY_MAX_WORDS words suggests, does not fail" \
|
||||
suggest "$(make_budget_fixture body-at-max "Short valid description." "$BODY_MAX_WORDS")" \
|
||||
suggest "$(make_budget_fixture body-at-max "$CLEAN_DESC" "$BODY_MAX_WORDS")" \
|
||||
"body is $BODY_MAX_WORDS words"
|
||||
expect_gate "body at $((BODY_MAX_WORDS + 1)) words fails" \
|
||||
fail "$(make_budget_fixture body-over-max "Short valid description." "$((BODY_MAX_WORDS + 1))")" \
|
||||
fail "$(make_budget_fixture body-over-max "$CLEAN_DESC" "$((BODY_MAX_WORDS + 1))")" \
|
||||
"$BODY_MAX_WORDS-word ceiling"
|
||||
|
||||
# The two word gates measure different things and must stay separable: a file
|
||||
@@ -382,7 +453,7 @@ BODY_ONLY_DESC="$(python3 -c "print(' '.join(['w'] * 100))")"
|
||||
expect_gate "frontmatter words do not count toward the $BODY_MAX_WORDS-word body ceiling" \
|
||||
suggest "$(make_budget_fixture body-independent "$BODY_ONLY_DESC" "$((BODY_MAX_WORDS - 5))")" \
|
||||
"words"
|
||||
BIG_BODY="$(make_budget_fixture body-over-not-whole-file "Short valid description." "$((BODY_MAX_WORDS + 1))")"
|
||||
BIG_BODY="$(make_budget_fixture body-over-not-whole-file "$CLEAN_DESC" "$((BODY_MAX_WORDS + 1))")"
|
||||
BIG_BODY_WORDS="$(wc -w < "$BIG_BODY")"
|
||||
if [[ "$BIG_BODY_WORDS" -le "$MAX_WORDS" ]]; then
|
||||
pass "the body-gate fixture is $BIG_BODY_WORDS whole-file words, well under MAX_WORDS=$MAX_WORDS — it fails on the body gate alone"
|
||||
@@ -393,21 +464,37 @@ fi
|
||||
echo ""
|
||||
echo "--- resolvable boundary targets ---"
|
||||
# Resolution is against the AUTHORING SOURCE (plugins/*/.apm/skills/ and
|
||||
# plugins/*/.apm/agents/), found here via the script's own repo root — these
|
||||
# fixtures live in a temp dir with no plugin tree of their own, so a resolving
|
||||
# target proves the repo-root path works.
|
||||
expect_gate "a boundary target naming a real skill resolves" \
|
||||
pass "$(make_budget_fixture target-ok \
|
||||
"Use when doing the thing. Do not use for commits — use git-commits instead." 10)"
|
||||
expect_gate "a boundary target naming a real AGENT resolves (agents are valid targets)" \
|
||||
pass "$(make_budget_fixture target-agent-ok \
|
||||
"Use when doing the thing. Do not use when the caller is an agent — invoke git-orchestrate instead." 10)"
|
||||
expect_gate "a boundary target that resolves to nothing fails" \
|
||||
fail "$(make_budget_fixture target-missing \
|
||||
"Use when doing the thing. Do not use for improvements — use no-such-skill-anywhere instead." 10)" \
|
||||
# plugins/*/.apm/agents/), reached by walking up FROM THE SKILL FILE. These
|
||||
# fixtures therefore build their own synthetic monorepo (make_tree_fixture) and
|
||||
# name only fixture-local targets: they must not depend on this repo's live
|
||||
# skills, or renaming git-commits would break a test about extraction grammar.
|
||||
expect_gate "a boundary target naming a sibling skill in the same package resolves" \
|
||||
pass "$(make_tree_fixture target-ok \
|
||||
"Use when doing the thing. Do not use for commits — use sibling-skill instead." 10)"
|
||||
expect_gate "a boundary target naming a skill in a SIBLING PLUGIN resolves (that is what a monorepo means)" \
|
||||
pass "$(make_tree_fixture target-cross-plugin \
|
||||
"Use when doing the thing. Do not use for the other thing — use cross-plugin-skill instead." 10)"
|
||||
expect_gate "a boundary target naming an AGENT resolves (agents are valid targets)" \
|
||||
pass "$(make_tree_fixture target-agent-ok \
|
||||
"Use when doing the thing. Do not use when the caller is an agent — invoke sibling-agent instead." 10)"
|
||||
# CORROBORATED: `sibling-skill` resolves in the same sentence, which is what
|
||||
# promotes a prose-form target from "reported" to "blocking". A lone prose-form
|
||||
# target is deliberately not fatal — see the case below and the shared resolver's
|
||||
# CORROBORATION note.
|
||||
expect_gate "a boundary target that resolves to nothing fails when its sentence names one that does" \
|
||||
fail "$(make_tree_fixture target-missing \
|
||||
"Use when doing the thing. Do not use for improvements — use sibling-skill or no-such-skill-anywhere instead." 10)" \
|
||||
"routes to 'no-such-skill-anywhere'"
|
||||
# UNCORROBORATED: identical grammar to the case above, and identical grammar to
|
||||
# "run `pre-commit` instead". Reported at SUGGESTION tier, exit 0 — a gate that
|
||||
# ships hot with no baseline and no suppression mechanism must not block a commit
|
||||
# on a token it cannot tell from a tool name.
|
||||
expect_gate "a lone boundary target that resolves to nothing is reported, not fatal" \
|
||||
suggest "$(make_tree_fixture target-missing-lone \
|
||||
"Use when doing the thing. Do not use for improvements — use no-such-lone-skill instead." 10)" \
|
||||
"routes to 'no-such-lone-skill'"
|
||||
expect_gate "a /slash-command boundary target that resolves to nothing fails" \
|
||||
fail "$(make_budget_fixture target-missing-slash \
|
||||
fail "$(make_tree_fixture target-missing-slash \
|
||||
"Use when doing the thing. Do not use for improvements — use /no-such-slash-skill instead." 10)" \
|
||||
"routes to 'no-such-slash-skill'"
|
||||
# False-positive guards. These phrasings are lifted from real descriptions:
|
||||
@@ -415,30 +502,64 @@ expect_gate "a /slash-command boundary target that resolves to nothing fails" \
|
||||
# gitea-files says "(use Read/Write/Edit)", gitea-labels-milestones says
|
||||
# "through `issue_write`/`pull_request_write`". None of them is a routing
|
||||
# target, and reading any of them as one makes the gate untrustworthy.
|
||||
#
|
||||
# Each carries a boundary clause in a SEPARATE sentence. That is not decoration:
|
||||
# target extraction is decided per sentence, so the clause satisfies the
|
||||
# missing-boundary-clause SUGGESTION (keeping the expected output empty) while
|
||||
# leaving the sentence under test outside a boundary context, which is the exact
|
||||
# condition each of these is about. They are built as trees so a universe exists
|
||||
# — in a bare temp dir the resolver would decline and the guard would pass
|
||||
# vacuously, proving nothing about extraction.
|
||||
expect_gate "'run pre-commit hooks' outside a boundary sentence is not a routing target" \
|
||||
pass "$(make_budget_fixture fp-precommit \
|
||||
"Use when the user wants to run pre-commit hooks or install git hooks." 10)"
|
||||
pass "$(make_tree_fixture fp-precommit \
|
||||
"Use when the user wants to run pre-commit hooks or install git hooks. Do not use for anything else." 10)"
|
||||
expect_gate "an arrow chain outside a boundary clause is not a routing target" \
|
||||
pass "$(make_budget_fixture fp-arrow \
|
||||
"Reproduce → minimise → instrument → fix → regression-test. Use when a bug is reported." 10)"
|
||||
pass "$(make_tree_fixture fp-arrow \
|
||||
"Reproduce → minimise → instrument → fix → regression-test. Use when a bug is reported. Do not use for anything else." 10)"
|
||||
expect_gate "tool names and MCP tool names are not routing targets" \
|
||||
pass "$(make_budget_fixture fp-tools \
|
||||
pass "$(make_tree_fixture fp-tools \
|
||||
"Use when writing issues. Do not use for local files (use Read/Write/Edit) — that write goes through \`issue_write\`/\`pull_request_write\` instead." 10)"
|
||||
|
||||
echo ""
|
||||
echo "--- the three live dangling routing targets are caught (issue #100) ---"
|
||||
# ADR-0020 records four broken routing targets and splits fixing them into its
|
||||
# own issue. Three are detectable from the description text alone; this asserts
|
||||
# the gate actually sees them rather than the check being vacuous in the corpus
|
||||
# it was written against.
|
||||
echo "--- with NO authoring root the resolver declines OUT LOUD and does not fail the file ---"
|
||||
# The consumer/draft case, and a real one: a SKILL.md in a bare directory with no
|
||||
# plugins/*/.apm/ above it and no .git has no universe to resolve against. The
|
||||
# required behaviour is neither a false FAIL nor silence — silence is how a whole
|
||||
# gate family goes missing unnoticed — so the INFO and the named unchecked target
|
||||
# are both asserted, along with exit 0. This is the same path make_tree_fixture
|
||||
# exists to escape, kept pinned so a future "just use the repo root" shortcut
|
||||
# (the ${BASH_SOURCE} universe leak ADR-0020 removed) fails here.
|
||||
expect_gate "a fixture with no authoring root reports DID NOT RUN and exits 0" \
|
||||
info "$(make_budget_fixture no-universe \
|
||||
"Use when doing the thing. Do not use for improvements — use some-other-skill instead." 10)" \
|
||||
"Unchecked target(s): some-other-skill"
|
||||
|
||||
echo ""
|
||||
echo "--- the live dangling routing targets are caught (issue #100) ---"
|
||||
# ADR-0020 records the broken routing targets and splits fixing them into its own
|
||||
# issue. This asserts the gate actually sees them rather than the check being
|
||||
# vacuous in the corpus it was written against.
|
||||
#
|
||||
# There used to be a third probe here, for `skill-improve` in skill-audit's
|
||||
# description. It was already stale: that target was fixed, so the iteration
|
||||
# permanently took a `pass "SKIP: ..."` branch — an assertion-free result counted
|
||||
# in the totals, which is worse than no probe at all because it makes the suite
|
||||
# look one test stronger than it is. It also contradicted
|
||||
# tests/test-adr0020-targets.sh, which pins the live dangling set as EXACTLY
|
||||
# {gitea-labels, neuledge-context}; that file is the authority on the set, this
|
||||
# one only checks the two are individually detected.
|
||||
#
|
||||
# Both SKIP branches are gone with it, for the same reason. A probe whose fixture
|
||||
# has been retrofitted is not "still passing" — it is a pin that needs updating,
|
||||
# here and in the exact-set assertion in test-adr0020-targets.sh, and it should
|
||||
# say so out loud rather than quietly agreeing with whatever it finds.
|
||||
for probe in \
|
||||
"plugins/bin/.apm/skills/research/SKILL.md:neuledge-context" \
|
||||
"plugins/kyberforge/.apm/skills/skill-audit/SKILL.md:skill-improve" \
|
||||
"plugins/gitea/.apm/skills/gitea-issues/SKILL.md:gitea-labels"; do
|
||||
probe_file="$REPO_ROOT/${probe%%:*}"
|
||||
probe_name="${probe##*:}"
|
||||
if [[ ! -f "$probe_file" ]]; then
|
||||
pass "SKIP: ${probe%%:*} no longer exists (retrofitted)"
|
||||
fail "the probe fixture ${probe%%:*} no longer exists — this pin has become vacuous; update it and EXPECTED_DANGLING in tests/test-adr0020-targets.sh together"
|
||||
continue
|
||||
fi
|
||||
# Captured, not piped: the script exits non-zero on these files and
|
||||
@@ -449,10 +570,8 @@ for probe in \
|
||||
set -e
|
||||
if [[ "$probe_out" == *"routes to '$probe_name'"* ]]; then
|
||||
pass "detects the dangling '$probe_name' target in ${probe%%:*}"
|
||||
elif ! grep -q "$probe_name" "$probe_file"; then
|
||||
pass "SKIP: '$probe_name' no longer appears in ${probe%%:*} (fixed by issue #100)"
|
||||
else
|
||||
fail "did not detect the dangling '$probe_name' target in ${probe%%:*}"
|
||||
fail "did not detect the dangling '$probe_name' target in ${probe%%:*}. If issue #100 retrofitted it, drop this probe and update EXPECTED_DANGLING in tests/test-adr0020-targets.sh; if a false-positive fix took a true positive with it, that is the regression this asserts."
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user