#!/usr/bin/env bash set -euo pipefail SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SKILL_ROOT="$(cd "$SKILL_DIR/.." && pwd)" TEMPLATES_DIR="$SKILL_ROOT/assets/templates" usage() { cat < Scaffold agent definition file(s) for Claude Code, GitHub Copilot CLI, and/or vendor-neutral APM packages. Arguments: agent-name Kebab-case agent identifier (e.g. code-reviewer, deploy-assistant). root Starting directory — scope is resolved by walking up from here: plugin/APM scope : nearest ancestor (at/above root) whose apm.yml has a top-level type: field (instructions, skill, hybrid, or prompts) — an apm.yml without type: is a marketplace-only manifest and is skipped, the walk continues upward → creates /.apm/agents/.agent.md (single vendor-neutral file — no tools, isolation, maxTurns, effort, memory, or permissionMode; apm compile has no per-target field integrator, see ADR-0016) → creates /sources.md (if absent) project scope : no type:-bearing apm.yml found; root is a project directory → creates /.claude/agents/.md → creates /.github/agents/.agent.md user scope : root is exactly ~ (home directory; checked directly, no walk-up) → creates ~/.claude/agents/.md → creates ~/.copilot/agents/.agent.md Each file is created only if it does not already exist (no-op per file). Exit codes: 0 Files created or already existed (no-op) 1 Invalid arguments, missing root, or templates not found EOF } if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then usage exit 0 fi if [[ $# -lt 2 ]]; then echo "Error: agent-name and root are required." >&2 echo "" >&2 usage >&2 exit 1 fi AGENT_NAME="$1" ROOT="$2" # Validate agent name format if ! echo "$AGENT_NAME" | grep -qE '^[a-z0-9]+(-[a-z0-9]+)*$'; then echo "Error: agent-name must use lowercase letters, numbers, and hyphens only." >&2 echo " No leading, trailing, or consecutive hyphens." >&2 echo " Received: '$AGENT_NAME'" >&2 exit 1 fi # Validate templates directory if [[ ! -d "$TEMPLATES_DIR" ]]; then echo "Error: templates directory not found at '$TEMPLATES_DIR'." >&2 echo " Run this script from its original location inside the agent-author skill." >&2 exit 1 fi # Expand tilde ROOT="${ROOT/#\~/$HOME}" # Validate root exists if [[ ! -d "$ROOT" ]]; then echo "Error: root directory '$ROOT' does not exist." >&2 exit 1 fi ROOT="$(cd "$ROOT" && pwd)" # --- Walk-up package-root detection --- # # Mirrors agent-audit's validate.sh scope walk-up, with apm.yml + type: swapped # in for the old plugin.json marker. Starting at ROOT, walk upward: # - an apm.yml with a top-level `type:` field marks an APM package root # (plugin/APM scope) — stop and return it. # - an apm.yml with no `type:` field is a marketplace-only manifest — skip # it, keep walking up. # - a .git directory marks the project-scope boundary — stop. # - filesystem root reached with neither found — boundary-reached. find_package_root() { local current="$1" while true; do if [[ -f "$current/apm.yml" ]] && grep -qE '^type:[[:space:]]*(instructions|skill|hybrid|prompts)([[:space:]]|$)' "$current/apm.yml"; then echo "plugin" echo "$current" return fi if [[ -d "$current/.git" ]]; then echo "project" echo "$current" return fi local parent parent="$(dirname "$current")" if [[ "$parent" == "$current" ]]; then echo "boundary-reached" echo "$current" return fi current="$parent" done } WALK_RESULT="$(find_package_root "$ROOT")" WALK_KIND="$(echo "$WALK_RESULT" | sed -n '1p')" WALK_ROOT="$(echo "$WALK_RESULT" | sed -n '2p')" PACKAGE_ROOT="" case "$WALK_KIND" in plugin) SCOPE="plugin" PACKAGE_ROOT="$WALK_ROOT" ;; project) SCOPE="project" ;; boundary-reached) if [[ "$ROOT" == "$HOME" ]]; then SCOPE="user" else # Default fallback, same as the pre-walk-up script: no plugin/APM # marker and not exactly $HOME means project scope. SCOPE="project" fi ;; esac # Determine file destinations case "$SCOPE" in plugin) APM_DIR="$PACKAGE_ROOT/.apm/agents" SOURCES_DIR="$PACKAGE_ROOT" ;; project) CC_DIR="$ROOT/.claude/agents" CP_DIR="$ROOT/.github/agents" SOURCES_DIR="" ;; user) CC_DIR="$HOME/.claude/agents" CP_DIR="$HOME/.copilot/agents" SOURCES_DIR="" ;; esac created_any=false if [[ "$SCOPE" == "plugin" ]]; then APM_FILE="$APM_DIR/$AGENT_NAME.agent.md" mkdir -p "$APM_DIR" if [[ -f "$APM_FILE" ]]; then echo "Skipping '$APM_FILE' — already exists." >&2 else sed "s/AGENT_NAME/$AGENT_NAME/g" "$TEMPLATES_DIR/apm-agent.md" > "$APM_FILE" echo "Created: $APM_FILE" >&2 created_any=true fi else CC_FILE="$CC_DIR/$AGENT_NAME.md" CP_FILE="$CP_DIR/$AGENT_NAME.agent.md" mkdir -p "$CC_DIR" mkdir -p "$CP_DIR" # Copy Claude Code template (no-op if exists) if [[ -f "$CC_FILE" ]]; then echo "Skipping '$CC_FILE' — already exists." >&2 else sed "s/AGENT_NAME/$AGENT_NAME/g" "$TEMPLATES_DIR/claude-code.md" > "$CC_FILE" echo "Created: $CC_FILE" >&2 created_any=true fi # Copy Copilot template (no-op if exists) if [[ -f "$CP_FILE" ]]; then echo "Skipping '$CP_FILE' — already exists." >&2 else sed "s/AGENT_NAME/$AGENT_NAME/g" "$TEMPLATES_DIR/copilot.agent.md" > "$CP_FILE" echo "Created: $CP_FILE" >&2 created_any=true fi fi # Create sources.md at plugin/APM package root (no-op if exists) if [[ -n "$SOURCES_DIR" ]]; then SOURCES_FILE="$SOURCES_DIR/sources.md" if [[ -f "$SOURCES_FILE" ]]; then echo "Skipping '$SOURCES_FILE' — already exists." >&2 else cat > "$SOURCES_FILE" <<'SOURCES' # Sources SOURCES echo "Created: $SOURCES_FILE" >&2 created_any=true fi fi if [[ "$created_any" == false ]]; then echo "All files already exist — nothing to do." >&2 else echo "" >&2 echo "Scope: $SCOPE" >&2 echo "" >&2 echo "Next steps:" >&2 if [[ "$SCOPE" == "plugin" ]]; then echo " 1. Fill in $APM_FILE — replace all FILL IN: placeholders (name, description, model, body only)" >&2 echo " 2. Populate $SOURCES_DIR/sources.md with research sources, or delete it" >&2 echo " 3. Validate: check required fields (name, description, system prompt) in the file" >&2 else echo " 1. Fill in $CC_FILE — replace all FILL IN: placeholders" >&2 echo " 2. Fill in $CP_FILE — replace all FILL IN: placeholders" >&2 echo " 3. Validate: check required fields (name, description, system prompt) in both files" >&2 fi fi