fix(kyberforge): align primitive-author and factory-audit rule tiers

Second clean-context audit found author Must/Should and audit FAIL/SUGGESTION
tiers drifting apart, and author Musts the audit never checked.

- factory-audit: FAIL on absolute or bare relative hook script paths, an
  applyTo present but empty, and unbalanced braces/brackets in applyTo;
  judgment steps for dependency stem collisions, helper .json in hook dirs,
  unresolvable instruction links, prompt model slugs and second-person
  bodies; an unmatched glob drops to SUGGESTION; deliberate tier deviations
  recorded in hook-flow.md; validate.sh --help lists the three new modes;
  DescriptionOpener message no longer prescribes "Use when".
- primitive-author: deprecated routing, extra prompt keys and the prompt
  description contract become Shoulds; hook Musts gain "contributes an
  entry", no bare relative paths, and executable-when-run-directly;
  prompt Must 1 covers hardlinks; Vale prose FAILs resolved at close.
- forge: say "hook, instruction or prompt" rather than "apm primitive".

Refs #94

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
This commit is contained in:
2026-09-28 18:02:05 +00:00
parent 9ac5340e15
commit 0ea3f69dc6
13 changed files with 211 additions and 47 deletions

View File

@@ -47,7 +47,8 @@ target:
- **`${PLUGIN_ROOT}`** is the target-neutral token; apm rewrites it per target
(`"${CLAUDE_PROJECT_DIR}/.claude/hooks/<pkg>/…"` on Claude, repo-relative elsewhere).
`${CLAUDE_PLUGIN_ROOT}` also works but ties the source to one harness's name.
`${CLAUDE_PLUGIN_ROOT}` is rewritten identically, so it is valid, but it ties the source to one
harness's name (Should 12).
- **Claude is the verified target.** apm 0.28.0 passes this nested shape to Copilot without
reshaping it, and whether Copilot CLI runs nested entries or honours `matcher` is unverified. That
gap is apm's to close. Per-file target routing is deprecated, so a Copilot-native flat hook
@@ -59,20 +60,23 @@ target:
Must:
1. The file sits directly in `.apm/hooks/`, is not a symlink, and parses as a JSON object. apm
skips invalid JSON silently.
skips invalid JSON silently. (apm also discovers a package-root `hooks/`, and `factory-audit`
accepts it for third-party packages; author in `.apm/hooks/`.)
2. Use the wrapped shape `{"hooks": {Event: [...]}}`. If a naked settings slice is used instead,
every top-level value must be a list, with no stray scalar keys anywhere.
3. Every event value is a list of objects, and every nested `hooks` is a list of objects. Anything
else fails the Copilot install outright.
else fails the Copilot install outright. The file contributes at least one entry: an empty one
deploys nothing, with only a warning.
4. Event names are PascalCase (`PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `SessionStart`,
`Stop`, …). An all-lowercase name (`stop`) never warns and never fires; a camelCase name outside
apm's rename map (`userPromptSubmit`) deploys verbatim to Claude and never fires.
5. The script is referenced as `${PLUGIN_ROOT}/…` (package root) or `./…` (hook directory),
exists inside the package, and is executable. No absolute path, and no `$` or backtick in the
path itself. A missing script is only a warning at install time. Executable is stricter than the
research's Should: a script invoked as the command's first token fails at runtime without it.
6. No `hooks-<target>` or `*-<target>-hooks` filename. That routing is deprecated; reach belongs to
`targets:` (see Gate).
5. The script is referenced as `${PLUGIN_ROOT}/…` (or `${CLAUDE_PLUGIN_ROOT}/…`, see Shape) for
the package root, or `./…` for the hook directory, and exists inside the package. No absolute
path and no bare relative path (`scripts/x.sh`): apm bundles and rewrites neither. No `$` or
backtick in the path itself. A missing script is only a warning at install time.
6. A script run directly as the command's first token is executable. This is stricter than the
research's Should: without it the hook fails every time it fires. A script passed to an
interpreter (`bash ${PLUGIN_ROOT}/x.sh`) needs no executable bit.
Should:
@@ -85,3 +89,6 @@ Should:
10. Quote a script path that may contain spaces: `"${PLUGIN_ROOT}/scripts/my hook.sh"`.
11. Keep helper files in the hook directory non-JSON. Copilot's loader rejects any bundled `.json`
without a `hooks` key.
12. Prefer `${PLUGIN_ROOT}` over `${CLAUDE_PLUGIN_ROOT}`.
13. No `hooks-<target>` or `*-<target>-hooks` filename. That routing is deprecated and reach belongs
to `targets:` (see Gate); the research allows it only when deprecated routing is intended.

View File

@@ -35,7 +35,8 @@ Must:
2. `description` is a non-empty string. apm only warns when it is missing.
3. The body is non-empty after trimming whitespace. apm deploys an empty rule silently.
4. `applyTo` is a non-empty glob or comma-separated list — top-level commas only as separators,
alternation inside `{}` (`"**/*.{ts,tsx}"`) — or absent after the Gate's explicit yes.
alternation inside `{}` (`"**/*.{ts,tsx}"`), braces and brackets balanced — or absent after the
Gate's explicit yes. An empty `applyTo: ""` is neither.
5. The stem is unique across the package and its dependencies: a `.claude/rules/<stem>.md`
collision is silently overwritten.
@@ -48,5 +49,6 @@ Should:
8. Put any rationale Claude needs in the body. `description` never reaches Claude — it survives only
for Copilot and as Cursor's index text.
9. Keep relative markdown links resolvable from the source file.
10. Check the glob against the tree: one that matches nothing never fires, and one broader than the
rule's real scope spends context on every file it touches.
10. Check the glob against the tree: one that matches nothing here fires only in consumer repos
that have such files, and one broader than the rule's real scope spends context on every file
it touches.

View File

@@ -40,25 +40,27 @@ Copy `assets/templates/name.prompt.md.template` and drop `.template` only on the
Must:
1. The path is `.apm/prompts/<name>.prompt.md`, directly in that directory, not a symlink. `<name>`
is a safe path segment and unique across `.apm/prompts/` and the package root; it becomes the
Copilot filename and the Claude `/command` name.
2. `description` is present and non-empty, per the contract above.
1. The path is `.apm/prompts/<name>.prompt.md`, directly in that directory, not a symlink or
hardlink. `<name>` is a safe path segment and unique across `.apm/prompts/` and the package
root; it becomes the Copilot filename and the Claude `/command` name.
2. `description` is present and non-empty.
3. Every `input:` name matches `^[A-Za-z][\w-]{0,63}$`, written in the object form
`- pr_number: "The PR to review"`. Never copy apm's published `- name: pr_number` /
`description: …` example: apm reads the map's keys, so it produces the arguments `name` and
`description`.
4. Every `${input:x}` in the body is declared in `input:`, and every declared name is used. Without
`input:`, no `${input:…}` may appear — it would reach Claude unrewritten.
5. Frontmatter keys stay within `description`, `allowed-tools`, `model`, `argument-hint` and
`input`. Claude drops everything else with only a warning. The one exception: a Copilot-only key
(`agent`, `tools`, …) that is intended, with its Claude drop accepted and said so.
Should:
6. Spell keys in kebab-case — `allowed-tools`, `argument-hint` — not the camelCase aliases.
7. Omit `argument-hint` when `input:` is set; apm synthesises `<a> <b>` from the input names.
8. Keep one intent per prompt, and write the body as second-person instructions.
9. Keep `description` to 250 characters or fewer.
10. Give `model` a slug the target accepts. Copilot ignores `allowed-tools` and `model`, so neither
5. Frontmatter keys stay within `description`, `allowed-tools`, `model`, `argument-hint` and
`input`. Claude drops everything else with only a warning. The exception: a Copilot-only key
(`agent`, `tools`, …) that is intended, with its Claude drop accepted and said so.
6. The description follows the contract above: one plain sentence, no trigger or boundary clause,
naming the skills or agents it steers.
7. Spell keys in kebab-case — `allowed-tools`, `argument-hint` — not the camelCase aliases.
8. Omit `argument-hint` when `input:` is set; apm synthesises `<a> <b>` from the input names.
9. Keep one intent per prompt, and write the body as second-person instructions.
10. Keep `description` to 250 characters or fewer.
11. Give `model` a slug the target accepts. Copilot ignores `allowed-tools` and `model`, so neither
constrains a Copilot run.