#!/usr/bin/env bash set -euo pipefail usage() { cat < Validate a Claude Code or Copilot agent file pair against the agent definition spec. Arguments: agent-file Path to either the Claude Code .md or Copilot .agent.md agent file. Exit codes: 0 All checks passed 1 One or more checks failed 2 Script error (unrecognized file extension or missing field-inventory.md) EOF } if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then usage exit 0 fi if [[ $# -lt 1 ]]; then echo "Error: agent-file is required." >&2 echo "" >&2 usage >&2 exit 1 fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" python3 -u - "$1" "$SCRIPT_DIR" <<'PYTHON' import sys import os import re agent_file = os.path.abspath(sys.argv[1]) script_dir = sys.argv[2] fname = os.path.basename(agent_file) # --- Detect provider (check .agent.md before .md) --- if fname.endswith('.agent.md'): provider = 'copilot' name_stem = fname[:-len('.agent.md')] elif fname.endswith('.md'): provider = 'claude-code' name_stem = fname[:-len('.md')] else: print(f"Error: unrecognized extension '{fname}' — expected .md or .agent.md", file=sys.stderr) sys.exit(2) # --- Load field-inventory.md --- inv_path = os.path.normpath(os.path.join(script_dir, '..', 'references', 'field-inventory.md')) if not os.path.isfile(inv_path): print(f"Error: field-inventory.md not found at {inv_path}", file=sys.stderr) sys.exit(2) with open(inv_path) as f: inv_content = f.read() def parse_section_tokens(content, section_name): lines = content.splitlines() for i, line in enumerate(lines): if line.strip() == f'## {section_name}': for j in range(i + 1, len(lines)): stripped = lines[j].strip() if stripped and not stripped.startswith('#') and not stripped.startswith('---'): return set(stripped.split()) return set() cc_only_fields = parse_section_tokens(inv_content, 'claude-code-only-fields') plugin_ignored_fields = parse_section_tokens(inv_content, 'plugin-silently-ignored-fields') # --- Detect scope --- def detect_scope(start_dir): current = os.path.abspath(start_dir) while True: if os.path.isfile(os.path.join(current, 'plugin.json')): return 'plugin', current if os.path.isdir(os.path.join(current, '.git')): return 'project', current parent = os.path.dirname(current) if parent == current: return 'user', os.path.expanduser('~') current = parent agent_dir = os.path.dirname(agent_file) scope, scope_root = detect_scope(agent_dir) # --- Derive counterpart path --- if scope == 'plugin': if provider == 'copilot': counterpart = os.path.join(agent_dir, name_stem + '.md') counterpart_provider = 'claude-code' else: counterpart = os.path.join(agent_dir, name_stem + '.agent.md') counterpart_provider = 'copilot' elif scope == 'project': if provider == 'claude-code': counterpart = os.path.join(scope_root, '.github', 'agents', name_stem + '.agent.md') counterpart_provider = 'copilot' else: counterpart = os.path.join(scope_root, '.claude', 'agents', name_stem + '.md') counterpart_provider = 'claude-code' else: # user home = os.path.expanduser('~') if provider == 'claude-code': counterpart = os.path.join(home, '.copilot', 'agents', name_stem + '.agent.md') counterpart_provider = 'copilot' else: counterpart = os.path.join(home, '.claude', 'agents', name_stem + '.md') counterpart_provider = 'claude-code' # --- Helpers --- failed = False def fail(msg): global failed failed = True print(f"FAIL {msg}") PLACEHOLDER_RE = re.compile(r'(?