Files
holocron/plugins/kyberforge/.apm/skills/factory-audit/scripts/lib-checks-primitive.sh
Defame1297 9ac5340e15 fix(kyberforge): resolve clean-context audit findings on primitive support
primitive-author:
- description excludes read-only review (-> factory-audit)
- validation Gotcha now matches the research: compile never reads
  prompts, install fails only on a bad Copilot hook payload and warns on
  prompt input names and dropped keys
- instruction fold-in into AGENTS.md/CLAUDE.md stated as conditional on
  dedup and --force-instructions
- hook checklist gains the wrapped-shape Must, drops hardlinks, notes
  why executable is stricter than the research, and states the
  separate Copilot-targeted package route instead of a blanket "don't"
- prompt Must 5 keeps the research's Copilot-only-key exception; adds
  model-slug and 250-char Shoulds; descriptions name skills or agents
- placeholder instruction covers both FILL IN and FILL_IN_ tokens

factory-audit: hardlink FAIL scoped to instructions and prompts
(find_hook_files skips symlinks only), with bats cases; prompt-flow
description rubric names skills or agents.

forge: version-bump, apm-routes and sources references updated for the
primitive route.

Refs #94

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
2026-09-28 17:40:12 +00:00

495 lines
21 KiB
Bash
Executable File

#!/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
# (.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
# primitive kind (hook | instruction | prompt) as argv[2].
#
# Every check here exists because apm itself does not make it. apm 0.28.0
# silently skips invalid hook JSON, only warns on an instruction with no
# 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
# 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.
#
# 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
# Claude, which is why it gets the two ADR-0029 SUGGESTIONs below — but whether
# a prompt body carries procedure that belongs in a skill is a judgment call the
# prompt flow makes by reading it, and deliberately has no heuristic here.
#
# Output follows lib-checks-agent.sh: FAIL lines on stderr, SUGGESTION and INFO
# on stdout, exit 1 on any FAIL, 0 otherwise.
#
# Consumed by: validate.sh, hook / instruction / prompt modes.
# shellcheck shell=bash
# shellcheck disable=SC2034
kyberforge_primitive_preflight() {
# Interpreter and library are checked separately so the message names the
# thing to install; see lib-checks-agent.sh for the history. PyYAML is needed
# for the two markdown kinds, and is required for hooks too so that one
# dependency set covers the whole suite rather than a hook audit passing on a
# machine where the next instruction audit cannot run.
if ! command -v python3 > /dev/null 2>&1; then
echo "Error: python3 is required but was not found on PATH." >&2
echo " Why: every primitive check parses the file; without python3 no check runs, and reporting that as a pass would be vacuous." >&2
echo " Fix: install python3." >&2
exit 2
fi
if ! python3 -c 'import yaml' > /dev/null 2>&1; then
echo "Error: PyYAML is required but is not importable by python3." >&2
echo " Why: instruction and prompt frontmatter has to be parsed the way apm parses it; a hand-rolled reader would disagree with it on exactly the edge cases these checks exist for." >&2
echo " Fix: python3 -m pip install PyYAML (or your distro's python3-yaml package)." >&2
exit 2
fi
}
IFS='' read -r -d '' KYBERFORGE_PRIMITIVE_PY <<'KYBERFORGE_PRIMITIVE' || true
import sys
import os
import re
import json
import yaml
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding='utf-8')
except AttributeError: # pragma: no cover — Python < 3.7
pass
target = os.path.abspath(sys.argv[1])
kind = sys.argv[2]
fname = os.path.basename(target)
parent_dir = os.path.dirname(target)
failed = False
suggestions = []
def fail(msg):
global failed
failed = True
print(f"FAIL {msg}", file=sys.stderr)
def suggest(msg):
suggestions.append(msg)
def info(msg):
print(f"INFO {msg}")
def read_text(path):
try:
with open(path, encoding='utf-8') as f:
return f.read()
except UnicodeDecodeError as exc:
fail(f"not valid UTF-8 ({exc.reason} at byte {exc.start}) — apm reads primitives as UTF-8 — {fname}")
except OSError as exc:
fail(f"cannot be read ({exc.strerror}) — {fname}")
return None
def check_not_linked(hardlinks=True):
# apm's find_files_by_glob (instructions, prompts) rejects symlinks and
# hardlinks (link count > 1); find_hook_files skips symlinks only, so hooks
# pass hardlinks=False. A rejected file is silently never deployed.
if os.path.islink(target):
fail(f"is a symlink — apm's discovery skips symlinks, so it is never deployed — {fname}")
return
if not hardlinks:
return
try:
if os.stat(target).st_nlink > 1:
fail(f"is a hardlink (link count > 1) — apm's discovery rejects hardlinks, so it is never deployed — {fname}")
except OSError:
pass
def package_root_for(subdir):
# <pkg>/.apm/<subdir>/<file> -> <pkg>. Returns None for any other layout.
if os.path.basename(parent_dir) != subdir:
return None
apm_dir = os.path.dirname(parent_dir)
if os.path.basename(apm_dir) != '.apm':
return None
return os.path.dirname(apm_dir)
FRONTMATTER_RE = re.compile(r'\A---[ \t]*\r?\n(.*?)\r?\n---[ \t]*(?:\r?\n|\Z)', re.DOTALL)
def split_frontmatter(content):
"""Return (frontmatter dict | None, body, ok). ok is False on a parse FAIL."""
if content.startswith('\ufeff'):
content = content[1:]
m = FRONTMATTER_RE.match(content)
if not m:
fail(f"has no YAML frontmatter block (--- ... ---) — description and every other key live there — {fname}")
return None, content, False
try:
fm = yaml.safe_load(m.group(1))
except yaml.YAMLError as exc:
mark = getattr(exc, 'problem_mark', None)
where = f" at line {mark.line + 2}" if mark is not None else ''
fail(f"frontmatter is not valid YAML{where} — apm cannot read any key from it — {fname}")
return None, content[m.end():], False
if fm is None:
fm = {}
if not isinstance(fm, dict):
fail(f"frontmatter is not a YAML mapping — {fname}")
return None, content[m.end():], False
return fm, content[m.end():], True
def check_description(fm):
desc = fm.get('description')
if not isinstance(desc, str) or not desc.strip():
fail(f"'description' is missing or empty — apm does not require it, so nothing else will catch this — {fname}")
return None
return desc.strip()
# ---------------------------------------------------------------------------
# Hooks
# ---------------------------------------------------------------------------
ROUTING_TOKENS = ('copilot', 'vscode', 'cursor', 'claude', 'codex', 'gemini',
'antigravity', 'windsurf', 'kiro')
_TOK = '|'.join(ROUTING_TOKENS)
ROUTING_STEM_RE = re.compile(rf'^hooks-(?:{_TOK})$|(?:^|-)(?:{_TOK})-hooks$')
# Claude's rename map, 0.28.0: the only camelCase names that reach Claude as a
# native event. Any other camelCase name is deployed verbatim and never fires.
CLAUDE_MAPPED_CAMEL = {'preToolUse', 'postToolUse', 'sessionStart', 'agentStop'}
HOOK_COMMAND_KEYS = ('command', 'bash', 'powershell', 'windows', 'linux', 'osx')
ROOT_TOKENS = ('PLUGIN_ROOT', 'CLAUDE_PLUGIN_ROOT', 'CURSOR_PLUGIN_ROOT', 'KIRO_PLUGIN_ROOT')
ROOT_TOKEN_RE = re.compile(r'\$\{(' + '|'.join(ROOT_TOKENS) + r')\}')
def extract_script_refs(cmd):
"""Yield (kind, relpath, is_first_token) for each package-relative script
reference in a hook command string. kind is 'root' for a ${*_PLUGIN_ROOT}
token, 'rel' for a leading ./path."""
refs = []
for m in ROOT_TOKEN_RE.finditer(cmd):
start, end = m.start(), m.end()
opened = start > 0 and cmd[start - 1] in '"\''
quote = cmd[start - 1] if opened else None
rest = cmd[end:]
if opened and rest.startswith(quote):
# split-quoted form: "${PLUGIN_ROOT}"/scripts/my\ hook.sh
rest = rest[1:]
path = re.match(r'((?:\\.|[^\s"\'])*)', rest).group(1).replace('\\', '')
elif opened:
path = rest.split(quote, 1)[0]
else:
path = re.match(r'((?:\\.|[^\s"\'])*)', rest).group(1).replace('\\', '')
prefix = cmd[:start - 1] if opened else cmd[:start]
refs.append(('root', path.lstrip('/'), not prefix.strip()))
stripped = cmd.lstrip().lstrip('"\'')
if stripped.startswith('./'):
path = re.match(r'((?:\\.|[^\s"\'])*)', stripped).group(1).replace('\\', '')
refs.append(('rel', path, True))
return refs
def check_script(kind_, rel, first, pkg_root, where):
if not rel:
return
if '$' in rel or '`' in rel:
fail(f"script path '{rel}' contains '$' or a backtick — apm refuses to rewrite it for Claude — {where}")
return
candidates = []
if kind_ == 'root':
candidates.append(os.path.join(pkg_root, rel))
else:
candidates.append(os.path.join(parent_dir, rel))
candidates.append(os.path.join(pkg_root, rel))
real_root = os.path.realpath(pkg_root)
found = None
for c in candidates:
real = os.path.realpath(c)
if real != real_root and not real.startswith(real_root + os.sep):
fail(f"script '{rel}' resolves outside the package — apm confines hook scripts to the package root — {where}")
return
if os.path.isfile(c):
found = c
break
if found is None:
fail(f"script '{rel}' does not exist in the package — apm only warns, then deploys a hook that fails every time it fires — {where}")
return
if first and not os.access(found, os.X_OK):
fail(f"script '{rel}' is run directly but is not executable — chmod +x it, or invoke it through an interpreter — {where}")
def audit_hook():
check_not_linked(hardlinks=False)
stem = fname[:-len('.json')]
hooks_dir = os.path.basename(parent_dir)
if hooks_dir != 'hooks':
fail(f"is not directly in a hooks/ directory — apm discovers hook files only at .apm/hooks/*.json and hooks/*.json, non-recursively — {fname}")
if os.path.basename(os.path.dirname(parent_dir)) == '.apm':
pkg_root = os.path.dirname(os.path.dirname(parent_dir))
else:
pkg_root = os.path.dirname(parent_dir)
if not os.path.isfile(os.path.join(pkg_root, 'apm.yml')):
info(f"no apm.yml at the inferred package root {pkg_root} — script paths are resolved against it anyway — {fname}")
if ROUTING_STEM_RE.search(stem):
suggest(f"filename stem '{stem}' uses deprecated hook filename routing — name it plainly and narrow reach with target:/targets: in the package's apm.yml — {fname}")
content = read_text(target)
if content is None:
return
try:
doc = json.loads(content)
except json.JSONDecodeError as exc:
fail(f"is not valid JSON (line {exc.lineno}, column {exc.colno}) — apm skips an unparseable hook file silently — {fname}")
return
if not isinstance(doc, dict):
fail(f"top level is not a JSON object — {fname}")
return
if 'hooks' in doc:
events = doc['hooks']
if not isinstance(events, dict):
fail(f"'hooks' is not an object — apm skips the file, and the Copilot install fails outright — {fname}")
return
else:
stray = [k for k, v in doc.items() if not isinstance(v, list)]
if stray:
fail(f"naked settings-slice shape with non-list top-level key(s) {', '.join(sorted(stray))} — apm does not promote it, Claude gets nothing and Copilot gets a junk file; wrap events in {{\"hooks\": {{...}}}} — {fname}")
return
events = doc
if not events:
fail(f"contributes no hook entries — apm warns and deploys nothing — {fname}")
return
# A file is Claude-shaped when its entries nest handlers under "hooks" or
# its handlers use "command"; the flat bash/powershell form is Copilot's.
claude_shaped = False
shape_ok = True
for event, entries in events.items():
if not isinstance(entries, list):
fail(f"event '{event}' is not a list — the Copilot install fails on this payload — {fname}")
shape_ok = False
continue
for i, entry in enumerate(entries):
if not isinstance(entry, dict):
fail(f"event '{event}' entry {i} is not an object — the Copilot install fails on this payload — {fname}")
shape_ok = False
continue
if 'hooks' in entry:
claude_shaped = True
nested = entry['hooks']
if not isinstance(nested, list) or not all(isinstance(h, dict) for h in nested):
fail(f"event '{event}' entry {i}: nested 'hooks' is not a list of objects — the Copilot install fails on this payload — {fname}")
shape_ok = False
elif 'command' in entry:
claude_shaped = True
for event in events:
if not event.strip():
fail(f"empty event name — {fname}")
elif not any(c.isupper() for c in event):
fail(f"event '{event}' is all-lowercase — no target maps it and apm never warns, so it silently never fires — {fname}")
elif claude_shaped and event[0].islower() and event not in CLAUDE_MAPPED_CAMEL:
fail(f"event '{event}' is camelCase in a Claude-shaped file and Claude's map does not rename it — it deploys verbatim and never fires; write it in PascalCase — {fname}")
if not shape_ok:
return
uses_claude_token = False
for event, entries in events.items():
for i, entry in enumerate(entries):
handlers = entry['hooks'] if 'hooks' in entry else [entry]
for j, handler in enumerate(handlers):
where = f"{fname} {event}[{i}]" + (f".hooks[{j}]" if 'hooks' in entry else '')
for key in HOOK_COMMAND_KEYS:
cmd = handler.get(key)
if not isinstance(cmd, str):
continue
if '${CLAUDE_PLUGIN_ROOT}' in cmd:
uses_claude_token = True
for kind_, rel, first in extract_script_refs(cmd):
check_script(kind_, rel, first, 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}")
# ---------------------------------------------------------------------------
# Instructions
# ---------------------------------------------------------------------------
INSTRUCTION_KEYS = {'description', 'applyTo', 'author', 'version'}
def audit_instruction():
check_not_linked()
stem = fname[:-len('.instructions.md')]
pkg_root = package_root_for('instructions')
if pkg_root is None:
fail(f"is not directly in a .apm/instructions/ directory — that is the authoring source; anything else is either deployed output or a legacy root file — {fname}")
else:
dup = os.path.join(pkg_root, fname)
if os.path.isfile(dup):
fail(f"stem '{stem}' also exists at the package root ({dup}) — both deploy to the same .claude/rules/{stem}.md, and one overwrites the other — {fname}")
content = read_text(target)
if content is None:
return
fm, body, ok = split_frontmatter(content)
if not ok:
return
check_description(fm)
if not body.strip():
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 == []:
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):
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)
if extra:
suggest(f"frontmatter key(s) {', '.join(extra)} are read by no target and dropped on Claude — keep to description and applyTo (author, version optional) — {fname}")
# ---------------------------------------------------------------------------
# Prompts
# ---------------------------------------------------------------------------
PROMPT_KEYS = {'description', 'allowed-tools', 'model', 'argument-hint', 'input'}
PROMPT_CAMEL_ALIASES = {'allowedTools': 'allowed-tools', 'argumentHint': 'argument-hint'}
INPUT_NAME_RE = re.compile(r'^[A-Za-z][\w-]{0,63}$')
# apm's own rewrite pattern for ${input:x}, command_integrator.py.
INPUT_REF_RE = re.compile(r'\$\{\{?\s*input\s*:\s*([\w-]+)\s*\}?\}')
TRIGGER_RE = re.compile(r'\buse\s+(?:this\s+)?when\b', re.IGNORECASE)
PROMPT_DESC_SUGGEST_CHARS = 250
def prompt_input_names(spec):
"""Mirror apm's _extract_input_names, but FAIL on what it rejects or
misreads instead of warning. Returns the declared names."""
names = []
def accept(candidate):
if not isinstance(candidate, str):
fail(f"input entry {candidate!r} is not a string name — apm rejects it — {fname}")
return
s = candidate.strip()
if not s:
return
if not INPUT_NAME_RE.match(s):
fail(f"input name '{s}' does not match ^[A-Za-z][\\w-]{{0,63}}$ — apm rejects it, so the argument never exists — {fname}")
return
names.append(s)
if spec is None:
return names
if isinstance(spec, str):
accept(spec)
elif isinstance(spec, dict):
for k in spec:
accept(k)
elif isinstance(spec, list):
for item in spec:
if isinstance(item, dict):
if len(item) > 1:
keys = ', '.join(str(k) for k in item)
hint = (" — this is the upstream docs example's `- name: x` / `description:` form, which yields arguments [name, description]"
if 'name' in item else '')
fail(f"input entry {{{keys}}} is one map with several keys — apm reads every key as an argument name{hint}; write `- <name>: \"<desc>\"` — {fname}")
for k in item:
accept(k)
else:
accept(item)
else:
fail(f"input is neither a name, a list nor a map — apm extracts no arguments from it — {fname}")
return names
def audit_prompt():
check_not_linked()
stem = fname[:-len('.prompt.md')]
segs = stem.replace('\\', '/').split('/')
if not stem.strip() or any(s in ('.', '..', '') for s in segs) or '/' in stem.replace('\\', '/'):
fail(f"name '{stem}' is not a safe path segment — apm's validate_path_segments rejects it — {fname}")
pkg_root = package_root_for('prompts')
if pkg_root is None:
fail(f"is not directly in a .apm/prompts/ directory — that is the authoring source; anything else is either deployed output or a legacy root file — {fname}")
else:
dup = os.path.join(pkg_root, fname)
if os.path.isfile(dup):
fail(f"name '{stem}' also exists at the package root ({dup}) — both deploy as /{stem}, and they collide — {fname}")
content = read_text(target)
if content is None:
return
fm, body, ok = split_frontmatter(content)
if not ok:
return
desc = check_description(fm)
if desc is not None:
if len(desc) > PROMPT_DESC_SUGGEST_CHARS:
suggest(f"description is {len(desc)} characters (> {PROMPT_DESC_SUGGEST_CHARS}) — it is one user-facing sentence (ADR-0029) — {fname}")
if TRIGGER_RE.search(desc):
suggest(f"description carries a 'Use when' trigger clause — a prompt is user-triggered (ADR-0029); a trigger clause invites the model to route to it on Claude — {fname}")
for camel, kebab in PROMPT_CAMEL_ALIASES.items():
if camel in fm:
suggest(f"'{camel}' — use the kebab-case spelling '{kebab}' apm documents — {fname}")
extra = sorted(k for k in fm if k not in PROMPT_KEYS and k not in PROMPT_CAMEL_ALIASES)
if extra:
suggest(f"frontmatter key(s) {', '.join(extra)} are dropped on Claude (it keeps only {', '.join(sorted(PROMPT_KEYS))}) — keep them only if the Copilot-only behaviour is intended — {fname}")
declared = prompt_input_names(fm.get('input'))
used = []
for m in INPUT_REF_RE.finditer(body):
if m.group(1) not in used:
used.append(m.group(1))
if used and not declared:
fail(f"body uses {', '.join('${input:' + u + '}' for u in used)} but no input: is declared — apm rewrites references only when input: names them, so Claude receives the literal text — {fname}")
else:
for u in used:
if u not in declared:
fail(f"body uses ${{input:{u}}} but input: does not declare '{u}' — {fname}")
for d in declared:
if d not in used:
fail(f"input '{d}' is declared but the body never uses ${{input:{d}}} — the user is asked for an argument that goes nowhere — {fname}")
if declared and ('argument-hint' in fm or 'argumentHint' in fm):
suggest(f"argument-hint is set alongside input: — apm synthesises the hint from input: names; drop it unless that form is inadequate — {fname}")
if kind == 'hook':
audit_hook()
elif kind == 'instruction':
audit_instruction()
elif kind == 'prompt':
audit_prompt()
else:
print(f"Error: unknown primitive kind '{kind}'", file=sys.stderr)
sys.exit(2)
for s in suggestions:
print(f"SUGGESTION {s}")
sys.exit(1 if failed else 0)
KYBERFORGE_PRIMITIVE
KYBERFORGE_PRIMITIVE_PY="${KYBERFORGE_PRIMITIVE_PY%$'\n'}"