#!/usr/bin/env bash # agent_scaffold — Crea un agente nuevo en agents_and_robots listo para arrancar. # Copia _template/, adapta config.yaml, valida skills, registra en Synapse, hace commit. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../shell/assert_command_exists.sh" # ============================================================ # HELPERS # ============================================================ _usage() { cat >&2 <<'EOF' Uso: agent_scaffold --display-name "" [opciones] Opciones: --display-name "" Nombre legible del agente (obligatorio) --skills cat/skill,... Habilitar skills (ej: devops/deploy-service) --llm openai|anthropic|claude-code LLM provider (default: openai) --model MODEL Modelo LLM (default segun provider) --description "..." Descripcion del agente --tags TAG1,TAG2 Tags separados por coma --no-register No registrar en Synapse --no-commit No hacer git commit --dry-run Solo mostrar el plan, sin modificar nada Salida: JSON con status, id, agent_dir, skills_enabled, registered, committed EOF exit 1 } _log() { echo "[agent_scaffold] $*"; } _warn() { echo "[agent_scaffold] WARN: $*" >&2; } _err() { echo "[agent_scaffold] ERROR: $*" >&2; return 1; } # Normaliza un valor YAML string (quita comillas si las tiene) _yaml_get() { local file="$1" key="$2" grep -E "^[[:space:]]*${key}:" "$file" 2>/dev/null | head -1 | sed 's/.*: *//' | tr -d '"' | tr -d "'" } # Reemplaza (o añade si no existe) una clave YAML de primer nivel. # Solo funciona para claves simples (no anidadas con sed). _yaml_set() { local file="$1" key="$2" value="$3" if grep -qE "^${key}:" "$file" 2>/dev/null; then sed -i "s|^${key}:.*|${key}: ${value}|" "$file" else echo "${key}: ${value}" >> "$file" fi } # Emite JSON de resultado _emit_json() { local status="$1" id="$2" agent_dir="$3" skills_json="$4" registered="$5" committed="$6" message="${7:-}" printf '{\n "status": "%s",\n "id": "%s",\n "agent_dir": "%s",\n "skills_enabled": %s,\n "registered": %s,\n "committed": %s' \ "$status" "$id" "$agent_dir" "$skills_json" "$registered" "$committed" if [[ -n "$message" ]]; then printf ',\n "message": "%s"' "$message" fi printf '\n}\n' } # ============================================================ # PARSE ARGS # ============================================================ agent_scaffold() { # Valores por defecto local id="" local display_name="" local skills_raw="" local llm_provider="openai" local llm_model="" local description="" local tags_raw="" local do_register=true local do_commit=true local dry_run=false if [[ $# -eq 0 ]]; then _usage; fi # Primer argumento positivo = id if [[ "$1" != --* ]]; then id="$1" shift fi while [[ $# -gt 0 ]]; do case "$1" in --display-name) display_name="$2"; shift 2 ;; --skills) skills_raw="$2"; shift 2 ;; --llm) llm_provider="$2"; shift 2 ;; --model) llm_model="$2"; shift 2 ;; --description) description="$2"; shift 2 ;; --tags) tags_raw="$2"; shift 2 ;; --no-register) do_register=false; shift ;; --no-commit) do_commit=false; shift ;; --dry-run) dry_run=true; shift ;; --display-name=*) display_name="${1#*=}"; shift ;; --skills=*) skills_raw="${1#*=}"; shift ;; --llm=*) llm_provider="${1#*=}"; shift ;; --model=*) llm_model="${1#*=}"; shift ;; --description=*) description="${1#*=}"; shift ;; --tags=*) tags_raw="${1#*=}"; shift ;; *) _err "Flag desconocido: $1" ;; esac done # ============================================================ # PASO 1: Validar contexto — localizar el proyecto # ============================================================ local fn_root="" if [[ -n "${FN_REGISTRY_ROOT:-}" && -d "$FN_REGISTRY_ROOT" ]]; then fn_root="$FN_REGISTRY_ROOT" elif [[ -f "$(pwd)/registry.db" ]]; then fn_root="$(pwd)" else _err "No se puede localizar fn_registry. Setea FN_REGISTRY_ROOT o ejecuta desde la raiz del registry." fi local project_dir="$fn_root/projects/element_agents/apps/agents_and_robots" if [[ ! -d "$project_dir" ]]; then _err "Proyecto agents_and_robots no encontrado en: $project_dir" fi local agents_dir="$project_dir/agents" local skills_base="$project_dir/skills" # ============================================================ # PASO 2: Validar id # ============================================================ if [[ -z "$id" ]]; then _err "El argumento es obligatorio. Uso: agent_scaffold --display-name \"Nombre\"" fi if [[ -z "$display_name" ]]; then _err "--display-name es obligatorio." fi # Verificar formato snake-case / kebab-case (sin espacios, solo alfanum y guiones) if [[ ! "$id" =~ ^[a-z0-9][a-z0-9_-]*[a-z0-9]$|^[a-z0-9]$ ]]; then _err "El id '$id' no es valido. Usar lowercase, sin espacios (ej: my-agent o my_agent)." fi # No debe existir ya if [[ -d "$agents_dir/$id" ]]; then _err "El agente '$id' ya existe en: $agents_dir/$id" fi # ============================================================ # Determinar modelo por defecto segun provider # ============================================================ if [[ -z "$llm_model" ]]; then case "$llm_provider" in openai) llm_model="gpt-4o" ;; anthropic) llm_model="claude-sonnet-4-20250514" ;; claude-code) llm_model="" ;; # claude-code no usa model directamente *) llm_model="gpt-4o" ;; esac fi # ============================================================ # Parsear skills # ============================================================ local -a skills_list=() if [[ -n "$skills_raw" ]]; then IFS=',' read -ra skills_list <<< "$skills_raw" fi # ============================================================ # Parsear tags # ============================================================ local tags_yaml="[]" if [[ -n "$tags_raw" ]]; then IFS=',' read -ra tags_arr <<< "$tags_raw" local tags_joined="" for t in "${tags_arr[@]}"; do t="${t// /}" # trim spaces tags_joined+="\"$t\", " done tags_yaml="[${tags_joined%, }]" fi # ============================================================ # PASO 5: Validar skills (antes del dry-run check para reportar errores) # ============================================================ local -a valid_skills=() local -a skill_categories=() if [[ ${#skills_list[@]} -gt 0 ]]; then for skill_path in "${skills_list[@]}"; do skill_path="${skill_path// /}" # trim spaces local skill_dir="$skills_base/$skill_path" if [[ ! -f "$skill_dir/SKILL.md" ]]; then _err "Skill '$skill_path' no encontrada. No existe: $skill_dir/SKILL.md"$'\n'"Skills disponibles:"$'\n'"$(find "$skills_base" -name 'SKILL.md' | sed "s|$skills_base/||" | sed 's|/SKILL.md||' | sort)" fi valid_skills+=("$skill_path") # Extraer categoria (primer componente del path) local cat="${skill_path%%/*}" # Añadir categoria si no esta ya local already=false for c in "${skill_categories[@]+"${skill_categories[@]}"}"; do [[ "$c" == "$cat" ]] && already=true && break done [[ "$already" == false ]] && skill_categories+=("$cat") done fi # ============================================================ # Construir JSON de skills para output # ============================================================ local skills_json="[]" if [[ ${#valid_skills[@]} -gt 0 ]]; then local sj="" for s in "${valid_skills[@]}"; do sj+="\"$s\", "; done skills_json="[${sj%, }]" fi local agent_dir_rel="projects/element_agents/apps/agents_and_robots/agents/$id" local agent_dir_abs="$agents_dir/$id" # ============================================================ # --dry-run: mostrar plan y salir # ============================================================ if [[ "$dry_run" == true ]]; then echo "=== DRY-RUN: agent_scaffold ===" echo "" echo " ID: $id" echo " Display name: $display_name" echo " LLM provider: $llm_provider" echo " LLM model: ${llm_model:-"(provider default)"}" echo " Description: ${description:-"(no description)"}" echo " Tags: ${tags_raw:-"(none)"}" echo " Skills: ${skills_raw:-"(none)"}" echo "" echo "Pasos que se ejecutarian:" echo " 1. cp -r $agents_dir/_template/ $agent_dir_abs/" echo " 2. rm -f $agent_dir_abs/template_para_llm.md $agent_dir_abs/PERSONALITIES.md" echo " 3. Editar config.yaml:" echo " agent.id: $id" echo " agent.name: $display_name" echo " agent.version: 0.1.0" echo " agent.template: false" [[ -n "$description" ]] && echo " agent.description: $description" [[ "$tags_yaml" != "[]" ]] && echo " agent.tags: $tags_yaml" echo " llm.primary.provider: $llm_provider" [[ -n "$llm_model" ]] && echo " llm.primary.model: $llm_model" if [[ ${#valid_skills[@]} -gt 0 ]]; then echo " skills.enabled: true" echo " skills.categories: [${skill_categories[*]}]" fi if [[ "$do_register" == true ]]; then echo " 4. Compilar bin/register si falta y ejecutar:" echo " bin/register --homeserver --username $id --displayname \"$display_name\" --env-var MATRIX_TOKEN_$(echo "$id" | tr '[:lower:]-' '[:upper:]_')" else echo " 4. (skip registro en Synapse)" fi if [[ "$do_commit" == true ]]; then echo " 5. git add agents/$id/ && git commit -m \"feat: scaffold agent $id\"" else echo " 5. (skip git commit)" fi echo "" echo "Output JSON esperado:" _emit_json "ok" "$id" "$agent_dir_rel" "$skills_json" "$do_register" "$do_commit" "dry-run" return 0 fi # ============================================================ # PASO 3: Copiar template # ============================================================ _log "Copiando template a agents/$id/ ..." cp -r "$agents_dir/_template/" "$agent_dir_abs/" # Eliminar archivos que son solo refs de la plantilla rm -f "$agent_dir_abs/template_para_llm.md" rm -f "$agent_dir_abs/PERSONALITIES.md" # Asegurar directorios obligatorios mkdir -p "$agent_dir_abs/prompts" "$agent_dir_abs/knowledge" # ============================================================ # PASO 4: Editar config.yaml # ============================================================ local config="$agent_dir_abs/config.yaml" _log "Editando config.yaml ..." # Campos de identidad del agente sed -i "s|^ id:.*| id: $id|" "$config" sed -i "s|^ name:.*| name: \"$display_name\"|" "$config" sed -i "s|^ version:.*| version: \"0.1.0\"|" "$config" sed -i "s|^ template:.*| template: false|" "$config" if [[ -n "$description" ]]; then sed -i "s|^ description:.*| description: \"$description\"|" "$config" fi if [[ "$tags_yaml" != "[]" ]]; then sed -i "s|^ tags:.*| tags: $tags_yaml|" "$config" fi # Actualizar personalidad sed -i "s|^ role:.*| role: \"$display_name\"|" "$config" # LLM provider y model # Usamos awk para editar el bloque llm.primary (más seguro para YAML anidado) local tmp_config tmp_config=$(mktemp) awk -v provider="$llm_provider" -v model="$llm_model" ' /^llm:/ { in_llm=1 } in_llm && /^ primary:/ { in_primary=1 } in_primary && /^ provider:/ { print " provider: " provider next } in_primary && /^ model:/ && model != "" { print " model: \"" model "\"" next } in_primary && /^ [a-z]/ { in_primary=0 } in_llm && /^[a-z]/ { in_llm=0; in_primary=0 } { print } ' "$config" > "$tmp_config" && mv "$tmp_config" "$config" # API key env segun provider local api_key_env="" case "$llm_provider" in openai) api_key_env="OPENAI_API_KEY" ;; anthropic) api_key_env="ANTHROPIC_API_KEY" ;; claude-code) api_key_env="" ;; esac if [[ -n "$api_key_env" ]]; then tmp_config=$(mktemp) awk -v env_var="$api_key_env" ' /^llm:/ { in_llm=1 } in_llm && /^ primary:/ { in_primary=1 } in_primary && /^ api_key_env:/ { print " api_key_env: " env_var next } in_primary && /^ [a-z]/ { in_primary=0 } in_llm && /^[a-z]/ { in_llm=0; in_primary=0 } { print } ' "$config" > "$tmp_config" && mv "$tmp_config" "$config" fi # Skills: actualizar el bloque skills: en config.yaml if [[ ${#valid_skills[@]} -gt 0 ]]; then local cats_yaml="" for c in "${skill_categories[@]}"; do cats_yaml+="\"$c\", "; done cats_yaml="[${cats_yaml%, }]" tmp_config=$(mktemp) awk -v cats="$cats_yaml" ' /^skills:/ { in_skills=1 } in_skills && /^ enabled:/ { print " enabled: true" next } in_skills && /^ categories:/ { print " categories: " cats next } in_skills && /^[a-z]/ { in_skills=0 } { print } ' "$config" > "$tmp_config" && mv "$tmp_config" "$config" fi # Matrix: actualizar homeserver, user_id, tokens local norm_id norm_id=$(echo "$id" | tr '[:lower:]-' '[:upper:]_') local homeserver="https://matrix-af2f3d.organic-machine.com" local server_name="matrix-af2f3d.organic-machine.com" sed -i "s|^ homeserver:.*| homeserver: \"$homeserver\"|" "$config" sed -i "s|^ user_id:.*| user_id: \"@${id}:${server_name}\"|" "$config" sed -i "s|^ access_token_env:.*| access_token_env: MATRIX_TOKEN_${norm_id}|" "$config" # Encryption sed -i "s|^ store_path:.*| store_path: \"./agents/${id}/data/crypto/\"|" "$config" sed -i "s|^ pickle_key_env:.*| pickle_key_env: PICKLE_KEY_${norm_id}|" "$config" sed -i "s|^ recovery_key_env:.*| recovery_key_env: SSSS_RECOVERY_KEY_${norm_id}|" "$config" _log "config.yaml actualizado." # ============================================================ # PASO 6: Crear/actualizar prompts/system.md si no existe o es el stub del template # ============================================================ local system_prompt="$agent_dir_abs/prompts/system.md" local needs_stub=false if [[ ! -f "$system_prompt" ]]; then needs_stub=true else # Si el archivo viene del template y es el stub generico, reemplazarlo if grep -q "Template Agent" "$system_prompt" 2>/dev/null; then needs_stub=true fi fi if [[ "$needs_stub" == true ]]; then cat > "$system_prompt" </dev/null; then if (cd "$project_dir" && go build -o bin/register ./cmd/register/ 2>&1); then _log "Compilado bin/register correctamente." else register_warn="No se pudo compilar bin/register. Registro omitido." _warn "$register_warn" fi else register_warn="go no encontrado en PATH (assert_command_exists fallo). Registro omitido." _warn "$register_warn" fi fi if [[ -x "$register_bin" ]]; then local admin_token="${MATRIX_ADMIN_TOKEN:-}" if [[ -z "$admin_token" ]]; then register_warn="MATRIX_ADMIN_TOKEN no esta definido. Registro omitido." _warn "$register_warn" else local env_var_name="MATRIX_TOKEN_${norm_id}" local register_out register_exit=0 register_out=$( cd "$project_dir" "$register_bin" \ --homeserver "$homeserver" \ --username "$id" \ --displayname "$display_name" \ --env-var "$env_var_name" \ 2>&1 ) || register_exit=$? if [[ $register_exit -eq 0 ]]; then registered=true _log "Agente registrado en Synapse." echo "$register_out" else register_warn="Registro en Synapse fallo (exit $register_exit). Agente creado pero sin credenciales Matrix." _warn "$register_warn" echo "$register_out" >&2 fi fi fi fi # ============================================================ # PASO 8: Commit (si no --no-commit) # ============================================================ local committed=false if [[ "$do_commit" == true ]]; then _log "Haciendo commit en el repo agents_and_robots ..." local git_exit=0 ( cd "$project_dir" git add "agents/$id/" 2>&1 git commit -m "feat: scaffold agent ${id} Agente creado con agent_scaffold: - display-name: ${display_name} - provider: ${llm_provider} - skills: ${skills_raw:-none} ${description:+"- description: ${description}"}" 2>&1 ) || git_exit=$? if [[ $git_exit -eq 0 ]]; then committed=true _log "Commit creado." else _warn "git commit fallo (exit $git_exit). El agente fue creado pero sin commit." fi fi # ============================================================ # PASO 9: Output JSON # ============================================================ local final_message="" [[ -n "$register_warn" ]] && final_message="$register_warn" _emit_json "ok" "$id" "$agent_dir_rel" "$skills_json" "$registered" "$committed" "$final_message" } # Ejecutar si es el script principal if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then agent_scaffold "$@" fi