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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user