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:
@@ -21,14 +21,14 @@ metadata:
|
||||
|
||||
## Step 1 — Dispatch
|
||||
|
||||
| Target or intent | Primitive | Reference |
|
||||
| Target or intent | Type | Reference |
|
||||
|---|---|---|
|
||||
| A hook — `.apm/hooks/<name>.json`, or "run X whenever Y happens" | hook | `references/hook.md` |
|
||||
| An instruction — `.apm/instructions/<name>.instructions.md`, or a rule for files matching a pattern | instruction | `references/instruction.md` |
|
||||
| A prompt — `.apm/prompts/<name>.prompt.md`, or a reusable message the user types to kick off work | prompt | `references/prompt.md` |
|
||||
| A skill or an agent | — | stop: route to `skill-author` or `agent-author` |
|
||||
|
||||
Read only the reference matching the resolved primitive — each is self-contained. If the target sits inside a git worktree, capture `rtk git log --oneline -1` before touching the filesystem; Step 4 needs it.
|
||||
Read only the reference matching the resolved type — each is self-contained. If the target sits inside a git worktree, capture `rtk git log --oneline -1` before touching the filesystem; Step 4 needs it.
|
||||
|
||||
## Step 2 — Boundary gate
|
||||
|
||||
@@ -46,7 +46,7 @@ Signals: grill output, `/factory-audit` findings, inline feedback, session conte
|
||||
|
||||
## Step 4 — Validate and close
|
||||
|
||||
1. Run `/factory-audit` on the file, inline in this context; resolve every FAIL before reporting done.
|
||||
1. Run `/factory-audit` on the file, inline in this context; resolve every FAIL before reporting done, including the `### Prose` FAILs Vale raises on an instruction or prompt body.
|
||||
2. Run `rtk apm install --dry-run` from the repo root and read what each target will receive. On a feature branch, discard `apm.lock.yaml` churn afterwards (`rtk git checkout -- apm.lock.yaml`).
|
||||
3. Bump the owning package's `apm.yml` `version:` — minor for a new primitive, patch for a fix — unless this branch already bumped it for unreleased work. A primitive has no version of its own.
|
||||
3. Bump the owning package's `apm.yml` `version:` — minor for a new hook, instruction or prompt, patch for a fix — unless this branch already bumped it for unreleased work. None of these has a version of its own.
|
||||
4. **Commit verification.** Inside a git worktree, once the audit is clean, run `rtk git add` and `rtk git commit`, then re-run `rtk git log --oneline -1` and confirm the hash changed from Step 1's. Staged-but-uncommitted work is silently lost if the tree is cleaned up. Outside a worktree, report done on a clean audit and name that as the reason.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user