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

@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# lib-checks-primitive.sh — SOURCED, never executed.
#
# The structural check suite for the three apm primitives that have no author
# skill of their own and no SKILL.md-shaped container: hooks
# The structural check suite for the three apm primitives with no
# SKILL.md-shaped container, all authored by primitive-author: hooks
# (.apm/hooks/*.json), instructions (*.instructions.md) and prompts
# (*.prompt.md). validate.sh detects which one it was handed from the path and
# feeds $KYBERFORGE_PRIMITIVE_PY to python3 with the target as argv[1] and the
@@ -13,9 +13,12 @@
# description or body, and never validates a prompt's input: names against its
# ${input:x} references — so `apm install` and `apm compile --validate` both exit
# 0 on files that deploy nothing, or deploy something that never fires. The
# checks and their tiers come from the Authoring checklists at the end of
# checks follow the Authoring checklists at the end of
# plugins/kyberforge/docs/research/docs/microsoft-apm/{hooks,instructions,prompt}-primitive-schema.md,
# which trace each rule to the apm source that makes it matter.
# which trace each rule to the apm source that makes it matter, except where
# references/{hook,instruction,prompt}-flow.md documents a deliberate deviation
# (a tier moved, or a check the research leaves audit-only). A Must in
# primitive-author is a FAIL here, a Should a SUGGESTION.
#
# No boundary resolver and no word budgets: none of these files is routed on a
# description the way a skill is. A prompt's description IS model-visible on
@@ -204,6 +207,37 @@ def extract_script_refs(cmd):
return refs
SCRIPT_EXT_RE = re.compile(r'\.(?:sh|bash|zsh|py|js|mjs|cjs|ts|ps1|rb|pl)$', re.IGNORECASE)
def first_token(cmd):
m = re.match(r'\s*(["\']?)((?:\\.|[^\s"\'])*)\1', cmd)
return m.group(2).replace('\\', '') if m else ''
def check_unanchored_script(cmd, pkg_root, where):
# apm rewrites and bundles only ${*_PLUGIN_ROOT}/... and ./... references;
# a bare command (`npx foo`, `echo hi`) passes through untouched, which is
# fine. An absolute script path, or a bare relative path to a file in the
# package, also passes through untouched — so the script is not bundled
# and the deployed hook points at a path that does not exist on the
# consumer's machine.
tok = first_token(cmd)
if not tok or tok.startswith('./') or '$' in tok or tok.startswith('~'):
return
if tok.startswith('/'):
real_root = os.path.realpath(pkg_root)
inside = os.path.realpath(tok).startswith(real_root + os.sep)
if inside or SCRIPT_EXT_RE.search(tok):
fail(f"script '{tok}' is an absolute path — apm neither bundles nor rewrites it, so it breaks on every other machine; reference it as ${{PLUGIN_ROOT}}/<path> — {where}")
return
if '/' in tok:
for base in (parent_dir, pkg_root):
if os.path.isfile(os.path.join(base, tok)):
fail(f"script '{tok}' is a bare relative path — apm bundles and rewrites only ${{PLUGIN_ROOT}}/... and ./... references, so this one deploys unbundled; prefix it with ${{PLUGIN_ROOT}}/ or ./ — {where}")
return
def check_script(kind_, rel, first, pkg_root, where):
if not rel:
return
@@ -323,8 +357,11 @@ def audit_hook():
continue
if '${CLAUDE_PLUGIN_ROOT}' in cmd:
uses_claude_token = True
for kind_, rel, first in extract_script_refs(cmd):
refs = extract_script_refs(cmd)
for kind_, rel, first in refs:
check_script(kind_, rel, first, pkg_root, where)
if not any(first for _, _, first in refs):
check_unanchored_script(cmd, pkg_root, where)
if uses_claude_token:
suggest(f"uses ${{CLAUDE_PLUGIN_ROOT}} — apm documents the target-neutral ${{PLUGIN_ROOT}}, which it rewrites identically for every target — {fname}")
@@ -337,6 +374,50 @@ def audit_hook():
INSTRUCTION_KEYS = {'description', 'applyTo', 'author', 'version'}
def split_top_level(value):
# apm's parse_apply_to: split on commas outside {} (and not escaped \,),
# strip each segment, drop empty ones.
segs, cur, depth, i = [], '', 0, 0
while i < len(value):
c = value[i]
if c == '\\' and i + 1 < len(value):
cur += value[i:i + 2]
i += 2
continue
if c == '{':
depth += 1
elif c == '}':
depth -= 1
if c == ',' and depth == 0:
segs.append(cur)
cur = ''
else:
cur += c
i += 1
segs.append(cur)
return [s.strip() for s in segs if s.strip()]
def check_apply_to(apply_to):
if isinstance(apply_to, list):
entries = [e for e in apply_to if e is not None and str(e).strip()]
globs = [str(e).strip() for e in entries]
elif isinstance(apply_to, str):
globs = split_top_level(apply_to)
else:
fail(f"applyTo is neither a string nor a list — apm cannot read a glob from it — {fname}")
return False
if not globs:
fail(f"applyTo is present but empty — remove the key for an intentionally always-on rule, or give it a glob — {fname}")
return False
ok = True
for g in globs:
if g.count('{') != g.count('}') or g.count('[') != g.count(']'):
fail(f"applyTo glob '{g}' has unbalanced braces or brackets — it matches nothing, so the rule never fires — {fname}")
ok = False
return ok
def audit_instruction():
check_not_linked()
stem = fname[:-len('.instructions.md')]
@@ -359,9 +440,10 @@ def audit_instruction():
fail(f"body is empty — apm deploys an empty rule without complaint — {fname}")
apply_to = fm.get('applyTo')
if apply_to is None or apply_to == '' or apply_to == []:
apply_to_ok = apply_to is not None and check_apply_to(apply_to)
if apply_to is None:
suggest(f"no applyTo — this loads into every session of every repo that installs the package; confirm always-on is intended, and that a rule for this repo alone is not really an AGENTS.md rule — {fname}")
elif isinstance(apply_to, list):
elif apply_to_ok and isinstance(apply_to, list):
suggest(f"applyTo is a YAML list — Copilot receives the file verbatim and its handling of a list is unverified; use one comma-separated string — {fname}")
extra = sorted(k for k in fm if k not in INSTRUCTION_KEYS)