Rewrite agent-development skill for opencode
- Update SKILL.md with JSON-first approach (agents.json pattern) - Add all opencode config options: mode, temperature, maxSteps, hidden, permission - Document permissions system with granular rules and glob patterns - Add references/opencode-agents-json-example.md with chiron pattern - Rewrite triggering-examples.md for opencode (Tab, @mention, Task tool) - Update agent-creation-system-prompt.md for JSON output format - Rewrite complete-agent-examples.md with JSON examples - Rewrite validate-agent.sh to support both JSON and Markdown validation
This commit is contained in:
304
skill/agent-development/scripts/validate-agent.sh
Executable file
304
skill/agent-development/scripts/validate-agent.sh
Executable file
@@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env bash
|
||||
# Agent Configuration Validator
|
||||
# Validates agent configurations in JSON or Markdown format
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <path/to/agents.json | path/to/agent.md>"
|
||||
echo ""
|
||||
echo "Validates agent configuration for Opencode:"
|
||||
echo " - JSON: Validates agents.json structure"
|
||||
echo " - Markdown: Validates agent .md file with frontmatter"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 agent/agents.json"
|
||||
echo " $0 ~/.config/opencode/agents/review.md"
|
||||
exit 1
|
||||
}
|
||||
|
||||
validate_json() {
|
||||
local file="$1"
|
||||
echo "🔍 Validating JSON agent configuration: $file"
|
||||
echo ""
|
||||
|
||||
# Check JSON syntax
|
||||
if ! python3 -c "import json; json.load(open('$file'))" 2>/dev/null; then
|
||||
echo "❌ Invalid JSON syntax"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Valid JSON syntax"
|
||||
|
||||
# Parse and validate each agent
|
||||
local error_count=0
|
||||
local warning_count=0
|
||||
|
||||
# Get agent names
|
||||
local agents
|
||||
agents=$(python3 -c "
|
||||
import json
|
||||
import sys
|
||||
|
||||
with open('$file') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Handle both formats: direct agents or nested under 'agent' key
|
||||
if 'agent' in data:
|
||||
agents = data['agent']
|
||||
else:
|
||||
agents = data
|
||||
|
||||
for name in agents.keys():
|
||||
print(name)
|
||||
")
|
||||
|
||||
if [ -z "$agents" ]; then
|
||||
echo "❌ No agents found in configuration"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Found agents: $agents"
|
||||
echo ""
|
||||
|
||||
# Validate each agent
|
||||
for agent_name in $agents; do
|
||||
echo "Checking agent: $agent_name"
|
||||
|
||||
local validation_result
|
||||
validation_result=$(python3 -c "
|
||||
import json
|
||||
import sys
|
||||
|
||||
with open('$file') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Handle both formats
|
||||
if 'agent' in data:
|
||||
agents = data['agent']
|
||||
else:
|
||||
agents = data
|
||||
|
||||
agent = agents.get('$agent_name', {})
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
# Check required field: description
|
||||
if 'description' not in agent:
|
||||
errors.append('Missing required field: description')
|
||||
elif len(agent['description']) < 10:
|
||||
warnings.append('Description is very short (< 10 chars)')
|
||||
|
||||
# Check mode if present
|
||||
mode = agent.get('mode', 'all')
|
||||
if mode not in ['primary', 'subagent', 'all']:
|
||||
errors.append(f'Invalid mode: {mode} (must be primary, subagent, or all)')
|
||||
|
||||
# Check model format if present
|
||||
model = agent.get('model', '')
|
||||
if model and '/' not in model:
|
||||
warnings.append(f'Model should use provider/model-id format: {model}')
|
||||
|
||||
# Check temperature if present
|
||||
temp = agent.get('temperature')
|
||||
if temp is not None:
|
||||
if not isinstance(temp, (int, float)):
|
||||
errors.append(f'Temperature must be a number: {temp}')
|
||||
elif temp < 0 or temp > 2:
|
||||
warnings.append(f'Temperature {temp} is outside typical range (0-1)')
|
||||
|
||||
# Check prompt
|
||||
prompt = agent.get('prompt', '')
|
||||
if prompt:
|
||||
if prompt.startswith('{file:') and not prompt.endswith('}'):
|
||||
errors.append('Invalid file reference syntax in prompt')
|
||||
elif 'prompt' not in agent:
|
||||
warnings.append('No prompt defined (will use default)')
|
||||
|
||||
# Check tools if present
|
||||
tools = agent.get('tools', {})
|
||||
if tools and not isinstance(tools, dict):
|
||||
errors.append('Tools must be an object')
|
||||
|
||||
# Check permission if present
|
||||
permission = agent.get('permission', {})
|
||||
if permission and not isinstance(permission, dict):
|
||||
errors.append('Permission must be an object')
|
||||
|
||||
# Output results
|
||||
for e in errors:
|
||||
print(f'ERROR:{e}')
|
||||
for w in warnings:
|
||||
print(f'WARNING:{w}')
|
||||
if not errors and not warnings:
|
||||
print('OK')
|
||||
")
|
||||
|
||||
while IFS= read -r line; do
|
||||
if [[ "$line" == ERROR:* ]]; then
|
||||
echo " ❌ ${line#ERROR:}"
|
||||
((error_count++))
|
||||
elif [[ "$line" == WARNING:* ]]; then
|
||||
echo " ⚠️ ${line#WARNING:}"
|
||||
((warning_count++))
|
||||
elif [[ "$line" == "OK" ]]; then
|
||||
echo " ✅ Valid"
|
||||
fi
|
||||
done <<< "$validation_result"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
if [ $error_count -eq 0 ] && [ $warning_count -eq 0 ]; then
|
||||
echo "✅ All agents validated successfully!"
|
||||
exit 0
|
||||
elif [ $error_count -eq 0 ]; then
|
||||
echo "⚠️ Validation passed with $warning_count warning(s)"
|
||||
exit 0
|
||||
else
|
||||
echo "❌ Validation failed with $error_count error(s) and $warning_count warning(s)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
validate_markdown() {
|
||||
local file="$1"
|
||||
echo "🔍 Validating Markdown agent file: $file"
|
||||
echo ""
|
||||
|
||||
# Check file exists
|
||||
if [ ! -f "$file" ]; then
|
||||
echo "❌ File not found: $file"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ File exists"
|
||||
|
||||
# Check starts with ---
|
||||
local first_line
|
||||
first_line=$(head -1 "$file")
|
||||
if [ "$first_line" != "---" ]; then
|
||||
echo "❌ File must start with YAML frontmatter (---)"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Starts with frontmatter"
|
||||
|
||||
# Check has closing ---
|
||||
if ! tail -n +2 "$file" | grep -q '^---$'; then
|
||||
echo "❌ Frontmatter not closed (missing second ---)"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Frontmatter properly closed"
|
||||
|
||||
local error_count=0
|
||||
local warning_count=0
|
||||
|
||||
# Extract and validate frontmatter
|
||||
local frontmatter
|
||||
frontmatter=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$file")
|
||||
|
||||
# Check description (required)
|
||||
if ! echo "$frontmatter" | grep -q '^description:'; then
|
||||
echo "❌ Missing required field: description"
|
||||
((error_count++))
|
||||
else
|
||||
echo "✅ description: present"
|
||||
fi
|
||||
|
||||
# Check mode if present
|
||||
local mode
|
||||
mode=$(echo "$frontmatter" | grep '^mode:' | sed 's/mode: *//' || true)
|
||||
if [ -n "$mode" ]; then
|
||||
case "$mode" in
|
||||
primary|subagent|all)
|
||||
echo "✅ mode: $mode"
|
||||
;;
|
||||
*)
|
||||
echo "❌ Invalid mode: $mode (must be primary, subagent, or all)"
|
||||
((error_count++))
|
||||
;;
|
||||
esac
|
||||
else
|
||||
echo "💡 mode: not specified (defaults to 'all')"
|
||||
fi
|
||||
|
||||
# Check model if present
|
||||
local model
|
||||
model=$(echo "$frontmatter" | grep '^model:' | sed 's/model: *//' || true)
|
||||
if [ -n "$model" ]; then
|
||||
if [[ "$model" == */* ]]; then
|
||||
echo "✅ model: $model"
|
||||
else
|
||||
echo "⚠️ model should use provider/model-id format: $model"
|
||||
((warning_count++))
|
||||
fi
|
||||
else
|
||||
echo "💡 model: not specified (will inherit)"
|
||||
fi
|
||||
|
||||
# Check temperature if present
|
||||
local temp
|
||||
temp=$(echo "$frontmatter" | grep '^temperature:' | sed 's/temperature: *//' || true)
|
||||
if [ -n "$temp" ]; then
|
||||
echo "✅ temperature: $temp"
|
||||
fi
|
||||
|
||||
# Check system prompt (body after frontmatter)
|
||||
local system_prompt
|
||||
system_prompt=$(awk '/^---$/{i++; next} i>=2' "$file")
|
||||
|
||||
if [ -z "$system_prompt" ]; then
|
||||
echo "⚠️ System prompt (body) is empty"
|
||||
((warning_count++))
|
||||
else
|
||||
local prompt_length=${#system_prompt}
|
||||
echo "✅ System prompt: $prompt_length characters"
|
||||
|
||||
if [ $prompt_length -lt 50 ]; then
|
||||
echo "⚠️ System prompt is very short"
|
||||
((warning_count++))
|
||||
fi
|
||||
|
||||
if ! echo "$system_prompt" | grep -q "You are\|You will\|Your"; then
|
||||
echo "⚠️ System prompt should use second person (You are..., You will...)"
|
||||
((warning_count++))
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
if [ $error_count -eq 0 ] && [ $warning_count -eq 0 ]; then
|
||||
echo "✅ Validation passed!"
|
||||
exit 0
|
||||
elif [ $error_count -eq 0 ]; then
|
||||
echo "⚠️ Validation passed with $warning_count warning(s)"
|
||||
exit 0
|
||||
else
|
||||
echo "❌ Validation failed with $error_count error(s) and $warning_count warning(s)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
if [ $# -eq 0 ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
FILE="$1"
|
||||
|
||||
if [ ! -f "$FILE" ]; then
|
||||
echo "❌ File not found: $FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine file type and validate
|
||||
if [[ "$FILE" == *.json ]]; then
|
||||
validate_json "$FILE"
|
||||
elif [[ "$FILE" == *.md ]]; then
|
||||
validate_markdown "$FILE"
|
||||
else
|
||||
echo "❌ Unknown file type. Expected .json or .md"
|
||||
echo ""
|
||||
usage
|
||||
fi
|
||||
Reference in New Issue
Block a user