@the-bearded-bear/claude-craft 5.6.0 → 5.7.0-next.bf058ea
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dev/i18n/en/Common/agents/ralph-conductor.md +50 -0
- package/Dev/i18n/en/Common/commands/full-audit.md +66 -0
- package/Dev/i18n/en/Common/commands/ralph-sprint.md +53 -1
- package/Dev/i18n/en/Common/commands/team-audit.md +310 -0
- package/Dev/i18n/en/Common/commands/team-security.md +325 -0
- package/Dev/i18n/en/Common/commands/team-sprint.md +243 -0
- package/README.md +10 -0
- package/Tools/AgentTeams/lib/compatibility-check.sh +883 -0
- package/Tools/AgentTeams/lib/cost-dashboard.sh +353 -0
- package/Tools/AgentTeams/lib/cost-estimator.sh +528 -0
- package/Tools/AgentTeams/lib/ralph-teams-adapter.sh +705 -0
- package/Tools/AgentTeams/lib/result-aggregator.sh +427 -0
- package/Tools/Ralph/lib/parallel-manager.sh +204 -18
- package/package.json +1 -1
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# Agent Teams - Compatibility Checker
|
|
4
|
+
# Validates that an agent's frontmatter (tools, model) meets a role's
|
|
5
|
+
# requirements in a team template. Prevents silent failures from tool
|
|
6
|
+
# restrictions (see devils-advocate.md Section 3.4).
|
|
7
|
+
# =============================================================================
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
# =============================================================================
|
|
12
|
+
# Constants
|
|
13
|
+
# =============================================================================
|
|
14
|
+
|
|
15
|
+
readonly VERSION="1.0.0"
|
|
16
|
+
readonly SCRIPT_NAME="$(basename "$0")"
|
|
17
|
+
|
|
18
|
+
# All known Claude Code tools
|
|
19
|
+
readonly ALL_TOOLS=(
|
|
20
|
+
"Read" "Write" "Edit" "Glob" "Grep" "Bash"
|
|
21
|
+
"Task" "NotebookEdit" "WebFetch" "WebSearch"
|
|
22
|
+
"SendMessage" "TaskCreate" "TaskUpdate" "TaskList" "TaskGet"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# Model capability tiers (higher = more capable)
|
|
26
|
+
declare -A MODEL_TIERS=(
|
|
27
|
+
["opus"]=3
|
|
28
|
+
["sonnet"]=2
|
|
29
|
+
["haiku"]=1
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Exit codes
|
|
33
|
+
readonly EXIT_OK=0
|
|
34
|
+
readonly EXIT_INCOMPATIBLE=1
|
|
35
|
+
readonly EXIT_USAGE=2
|
|
36
|
+
readonly EXIT_FILE_ERROR=3
|
|
37
|
+
|
|
38
|
+
# Colors (disabled if not a terminal)
|
|
39
|
+
if [[ -t 1 ]]; then
|
|
40
|
+
readonly RED='\033[0;31m'
|
|
41
|
+
readonly GREEN='\033[0;32m'
|
|
42
|
+
readonly YELLOW='\033[0;33m'
|
|
43
|
+
readonly BLUE='\033[0;34m'
|
|
44
|
+
readonly BOLD='\033[1m'
|
|
45
|
+
readonly NC='\033[0m'
|
|
46
|
+
else
|
|
47
|
+
readonly RED=''
|
|
48
|
+
readonly GREEN=''
|
|
49
|
+
readonly YELLOW=''
|
|
50
|
+
readonly BLUE=''
|
|
51
|
+
readonly BOLD=''
|
|
52
|
+
readonly NC=''
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# =============================================================================
|
|
56
|
+
# Usage
|
|
57
|
+
# =============================================================================
|
|
58
|
+
|
|
59
|
+
show_help() {
|
|
60
|
+
cat <<EOF
|
|
61
|
+
${BOLD}${SCRIPT_NAME}${NC} v${VERSION} - Agent Teams Compatibility Checker
|
|
62
|
+
|
|
63
|
+
Validates that an agent's frontmatter configuration (tools, model, skills)
|
|
64
|
+
meets the requirements of a specific role in a team template.
|
|
65
|
+
|
|
66
|
+
${BOLD}USAGE${NC}
|
|
67
|
+
${SCRIPT_NAME} --agent <agent.md> --require-tools <tool1,tool2,...> [OPTIONS]
|
|
68
|
+
${SCRIPT_NAME} --agent <agent.md> --role-file <role.yaml> [OPTIONS]
|
|
69
|
+
${SCRIPT_NAME} --agent-dir <dir> --require-tools <tools> [--batch]
|
|
70
|
+
|
|
71
|
+
${BOLD}OPTIONS${NC}
|
|
72
|
+
-a, --agent <path> Path to agent .md file with YAML frontmatter
|
|
73
|
+
-d, --agent-dir <dir> Directory of agent .md files (batch mode)
|
|
74
|
+
-t, --require-tools <list> Comma-separated list of required tools
|
|
75
|
+
-m, --require-model <tier> Minimum model tier: haiku, sonnet, opus
|
|
76
|
+
-s, --require-skills <list> Comma-separated list of required skills
|
|
77
|
+
-r, --role-file <path> YAML file defining role requirements
|
|
78
|
+
-b, --batch Check all agents in --agent-dir against requirements
|
|
79
|
+
-n, --dry-run Show what would be checked without running validation
|
|
80
|
+
-q, --quiet Only output errors (exit code indicates result)
|
|
81
|
+
-j, --json Output results as JSON
|
|
82
|
+
-h, --help Show this help message
|
|
83
|
+
-v, --version Show version
|
|
84
|
+
|
|
85
|
+
${BOLD}ROLE FILE FORMAT${NC} (YAML)
|
|
86
|
+
required_tools:
|
|
87
|
+
- Edit
|
|
88
|
+
- Write
|
|
89
|
+
- Bash
|
|
90
|
+
minimum_model: sonnet
|
|
91
|
+
required_skills:
|
|
92
|
+
- testing
|
|
93
|
+
disallowed_tools_ok: false
|
|
94
|
+
|
|
95
|
+
${BOLD}EXAMPLES${NC}
|
|
96
|
+
# Check if tdd-coach can perform implementation work
|
|
97
|
+
${SCRIPT_NAME} --agent agents/tdd-coach.md --require-tools Edit,Write,Bash
|
|
98
|
+
|
|
99
|
+
# Check minimum model tier
|
|
100
|
+
${SCRIPT_NAME} --agent agents/research-assistant.md --require-model sonnet
|
|
101
|
+
|
|
102
|
+
# Batch check all agents for audit capability
|
|
103
|
+
${SCRIPT_NAME} --agent-dir agents/ --require-tools Read,Glob,Grep,Bash --batch
|
|
104
|
+
|
|
105
|
+
# Dry run to see what would be validated
|
|
106
|
+
${SCRIPT_NAME} --agent agents/tdd-coach.md --require-tools Edit,Write --dry-run
|
|
107
|
+
|
|
108
|
+
# Use a role definition file
|
|
109
|
+
${SCRIPT_NAME} --agent agents/tdd-coach.md --role-file roles/implementer.yaml
|
|
110
|
+
|
|
111
|
+
${BOLD}EXIT CODES${NC}
|
|
112
|
+
0 Agent is compatible with role requirements
|
|
113
|
+
1 Agent is incompatible (missing tools, insufficient model, etc.)
|
|
114
|
+
2 Usage error (invalid arguments)
|
|
115
|
+
3 File error (agent file not found, parse error)
|
|
116
|
+
|
|
117
|
+
EOF
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
show_version() {
|
|
121
|
+
echo "${SCRIPT_NAME} v${VERSION}"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# =============================================================================
|
|
125
|
+
# Logging
|
|
126
|
+
# =============================================================================
|
|
127
|
+
|
|
128
|
+
log_info() {
|
|
129
|
+
[[ "${QUIET:-false}" == "true" ]] && return
|
|
130
|
+
printf "${BLUE}[INFO]${NC} %s\n" "$1"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
log_ok() {
|
|
134
|
+
[[ "${QUIET:-false}" == "true" ]] && return
|
|
135
|
+
printf "${GREEN}[PASS]${NC} %s\n" "$1"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
log_warn() {
|
|
139
|
+
printf "${YELLOW}[WARN]${NC} %s\n" "$1"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
log_fail() {
|
|
143
|
+
printf "${RED}[FAIL]${NC} %s\n" "$1"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
log_error() {
|
|
147
|
+
printf "${RED}[ERROR]${NC} %s\n" "$1" >&2
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# =============================================================================
|
|
151
|
+
# Frontmatter Parser
|
|
152
|
+
# Extracts YAML frontmatter between --- delimiters from an agent .md file
|
|
153
|
+
# =============================================================================
|
|
154
|
+
|
|
155
|
+
# Parse the YAML frontmatter and set global variables:
|
|
156
|
+
# AGENT_NAME, AGENT_MODEL, AGENT_TOOLS[], AGENT_DISALLOWED_TOOLS[], AGENT_SKILLS[]
|
|
157
|
+
parse_agent_frontmatter() {
|
|
158
|
+
local agent_file="$1"
|
|
159
|
+
|
|
160
|
+
if [[ ! -f "$agent_file" ]]; then
|
|
161
|
+
log_error "Agent file not found: $agent_file"
|
|
162
|
+
return $EXIT_FILE_ERROR
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
# Read the frontmatter block (between first and second ---)
|
|
166
|
+
local in_frontmatter=false
|
|
167
|
+
local frontmatter=""
|
|
168
|
+
|
|
169
|
+
while IFS= read -r line; do
|
|
170
|
+
if [[ "$line" == "---" ]]; then
|
|
171
|
+
if [[ "$in_frontmatter" == "true" ]]; then
|
|
172
|
+
break # End of frontmatter
|
|
173
|
+
else
|
|
174
|
+
in_frontmatter=true
|
|
175
|
+
continue
|
|
176
|
+
fi
|
|
177
|
+
fi
|
|
178
|
+
if [[ "$in_frontmatter" == "true" ]]; then
|
|
179
|
+
frontmatter+="$line"$'\n'
|
|
180
|
+
fi
|
|
181
|
+
done < "$agent_file"
|
|
182
|
+
|
|
183
|
+
if [[ -z "$frontmatter" ]]; then
|
|
184
|
+
log_error "No YAML frontmatter found in: $agent_file"
|
|
185
|
+
return $EXIT_FILE_ERROR
|
|
186
|
+
fi
|
|
187
|
+
|
|
188
|
+
# Parse individual fields
|
|
189
|
+
AGENT_NAME=""
|
|
190
|
+
AGENT_MODEL=""
|
|
191
|
+
AGENT_TOOLS=()
|
|
192
|
+
AGENT_DISALLOWED_TOOLS=()
|
|
193
|
+
AGENT_SKILLS=()
|
|
194
|
+
AGENT_HAS_MEMORY=""
|
|
195
|
+
|
|
196
|
+
local current_list=""
|
|
197
|
+
|
|
198
|
+
while IFS= read -r line; do
|
|
199
|
+
# Skip empty lines and comments
|
|
200
|
+
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
|
|
201
|
+
|
|
202
|
+
# Detect top-level keys (no leading whitespace)
|
|
203
|
+
if [[ "$line" =~ ^([a-zA-Z_-]+):[[:space:]]*(.*) ]]; then
|
|
204
|
+
local key="${BASH_REMATCH[1]}"
|
|
205
|
+
local value="${BASH_REMATCH[2]}"
|
|
206
|
+
|
|
207
|
+
# Trim whitespace
|
|
208
|
+
value="${value#"${value%%[![:space:]]*}"}"
|
|
209
|
+
value="${value%"${value##*[![:space:]]}"}"
|
|
210
|
+
|
|
211
|
+
case "$key" in
|
|
212
|
+
name)
|
|
213
|
+
AGENT_NAME="$value"
|
|
214
|
+
current_list=""
|
|
215
|
+
;;
|
|
216
|
+
model)
|
|
217
|
+
AGENT_MODEL="$value"
|
|
218
|
+
current_list=""
|
|
219
|
+
;;
|
|
220
|
+
memory)
|
|
221
|
+
AGENT_HAS_MEMORY="$value"
|
|
222
|
+
current_list=""
|
|
223
|
+
;;
|
|
224
|
+
tools)
|
|
225
|
+
current_list="tools"
|
|
226
|
+
# Handle inline list: tools: [Read, Write]
|
|
227
|
+
if [[ "$value" =~ ^\[.*\]$ ]]; then
|
|
228
|
+
local items="${value#[}"
|
|
229
|
+
items="${items%]}"
|
|
230
|
+
IFS=',' read -ra parts <<< "$items"
|
|
231
|
+
for part in "${parts[@]}"; do
|
|
232
|
+
part="${part#"${part%%[![:space:]]*}"}"
|
|
233
|
+
part="${part%"${part##*[![:space:]]}"}"
|
|
234
|
+
[[ -n "$part" ]] && AGENT_TOOLS+=("$part")
|
|
235
|
+
done
|
|
236
|
+
current_list=""
|
|
237
|
+
fi
|
|
238
|
+
;;
|
|
239
|
+
disallowedTools)
|
|
240
|
+
current_list="disallowed"
|
|
241
|
+
if [[ "$value" =~ ^\[.*\]$ ]]; then
|
|
242
|
+
local items="${value#[}"
|
|
243
|
+
items="${items%]}"
|
|
244
|
+
IFS=',' read -ra parts <<< "$items"
|
|
245
|
+
for part in "${parts[@]}"; do
|
|
246
|
+
part="${part#"${part%%[![:space:]]*}"}"
|
|
247
|
+
part="${part%"${part##*[![:space:]]}"}"
|
|
248
|
+
[[ -n "$part" ]] && AGENT_DISALLOWED_TOOLS+=("$part")
|
|
249
|
+
done
|
|
250
|
+
current_list=""
|
|
251
|
+
fi
|
|
252
|
+
;;
|
|
253
|
+
skills)
|
|
254
|
+
current_list="skills"
|
|
255
|
+
if [[ "$value" =~ ^\[.*\]$ ]]; then
|
|
256
|
+
local items="${value#[}"
|
|
257
|
+
items="${items%]}"
|
|
258
|
+
IFS=',' read -ra parts <<< "$items"
|
|
259
|
+
for part in "${parts[@]}"; do
|
|
260
|
+
part="${part#"${part%%[![:space:]]*}"}"
|
|
261
|
+
part="${part%"${part##*[![:space:]]}"}"
|
|
262
|
+
[[ -n "$part" ]] && AGENT_SKILLS+=("$part")
|
|
263
|
+
done
|
|
264
|
+
current_list=""
|
|
265
|
+
fi
|
|
266
|
+
;;
|
|
267
|
+
permissionMode|description)
|
|
268
|
+
current_list=""
|
|
269
|
+
;;
|
|
270
|
+
*)
|
|
271
|
+
current_list=""
|
|
272
|
+
;;
|
|
273
|
+
esac
|
|
274
|
+
# Detect list items ( - value)
|
|
275
|
+
elif [[ "$line" =~ ^[[:space:]]+-[[:space:]]+(.*) ]]; then
|
|
276
|
+
local item="${BASH_REMATCH[1]}"
|
|
277
|
+
item="${item#"${item%%[![:space:]]*}"}"
|
|
278
|
+
item="${item%"${item##*[![:space:]]}"}"
|
|
279
|
+
|
|
280
|
+
case "$current_list" in
|
|
281
|
+
tools)
|
|
282
|
+
AGENT_TOOLS+=("$item")
|
|
283
|
+
;;
|
|
284
|
+
disallowed)
|
|
285
|
+
AGENT_DISALLOWED_TOOLS+=("$item")
|
|
286
|
+
;;
|
|
287
|
+
skills)
|
|
288
|
+
AGENT_SKILLS+=("$item")
|
|
289
|
+
;;
|
|
290
|
+
esac
|
|
291
|
+
else
|
|
292
|
+
# Non-list, non-key line resets list context
|
|
293
|
+
current_list=""
|
|
294
|
+
fi
|
|
295
|
+
done <<< "$frontmatter"
|
|
296
|
+
|
|
297
|
+
return 0
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# =============================================================================
|
|
301
|
+
# Role Requirements Parser (from YAML file)
|
|
302
|
+
# =============================================================================
|
|
303
|
+
|
|
304
|
+
parse_role_file() {
|
|
305
|
+
local role_file="$1"
|
|
306
|
+
|
|
307
|
+
if [[ ! -f "$role_file" ]]; then
|
|
308
|
+
log_error "Role file not found: $role_file"
|
|
309
|
+
return $EXIT_FILE_ERROR
|
|
310
|
+
fi
|
|
311
|
+
|
|
312
|
+
ROLE_REQUIRED_TOOLS=()
|
|
313
|
+
ROLE_MINIMUM_MODEL=""
|
|
314
|
+
ROLE_REQUIRED_SKILLS=()
|
|
315
|
+
|
|
316
|
+
local current_list=""
|
|
317
|
+
|
|
318
|
+
while IFS= read -r line; do
|
|
319
|
+
[[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue
|
|
320
|
+
|
|
321
|
+
if [[ "$line" =~ ^([a-zA-Z_-]+):[[:space:]]*(.*) ]]; then
|
|
322
|
+
local key="${BASH_REMATCH[1]}"
|
|
323
|
+
local value="${BASH_REMATCH[2]}"
|
|
324
|
+
value="${value#"${value%%[![:space:]]*}"}"
|
|
325
|
+
value="${value%"${value##*[![:space:]]}"}"
|
|
326
|
+
|
|
327
|
+
case "$key" in
|
|
328
|
+
required_tools)
|
|
329
|
+
current_list="req_tools"
|
|
330
|
+
;;
|
|
331
|
+
minimum_model)
|
|
332
|
+
ROLE_MINIMUM_MODEL="$value"
|
|
333
|
+
current_list=""
|
|
334
|
+
;;
|
|
335
|
+
required_skills)
|
|
336
|
+
current_list="req_skills"
|
|
337
|
+
;;
|
|
338
|
+
*)
|
|
339
|
+
current_list=""
|
|
340
|
+
;;
|
|
341
|
+
esac
|
|
342
|
+
elif [[ "$line" =~ ^[[:space:]]+-[[:space:]]+(.*) ]]; then
|
|
343
|
+
local item="${BASH_REMATCH[1]}"
|
|
344
|
+
item="${item#"${item%%[![:space:]]*}"}"
|
|
345
|
+
item="${item%"${item##*[![:space:]]}"}"
|
|
346
|
+
|
|
347
|
+
case "$current_list" in
|
|
348
|
+
req_tools)
|
|
349
|
+
ROLE_REQUIRED_TOOLS+=("$item")
|
|
350
|
+
;;
|
|
351
|
+
req_skills)
|
|
352
|
+
ROLE_REQUIRED_SKILLS+=("$item")
|
|
353
|
+
;;
|
|
354
|
+
esac
|
|
355
|
+
fi
|
|
356
|
+
done < "$role_file"
|
|
357
|
+
|
|
358
|
+
return 0
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
# =============================================================================
|
|
362
|
+
# Validation Logic
|
|
363
|
+
# =============================================================================
|
|
364
|
+
|
|
365
|
+
# Check if a tool is in the agent's effective tool set
|
|
366
|
+
# An agent has a tool if:
|
|
367
|
+
# - tools list is empty (no restrictions, all tools available)
|
|
368
|
+
# - tool is in the tools list AND not in disallowedTools
|
|
369
|
+
agent_has_tool() {
|
|
370
|
+
local tool="$1"
|
|
371
|
+
|
|
372
|
+
# Check disallowed first (always takes precedence)
|
|
373
|
+
for disallowed in "${AGENT_DISALLOWED_TOOLS[@]}"; do
|
|
374
|
+
if [[ "$disallowed" == "$tool" ]]; then
|
|
375
|
+
return 1
|
|
376
|
+
fi
|
|
377
|
+
done
|
|
378
|
+
|
|
379
|
+
# If no tools list specified, agent has all tools (default)
|
|
380
|
+
if [[ ${#AGENT_TOOLS[@]} -eq 0 ]]; then
|
|
381
|
+
return 0
|
|
382
|
+
fi
|
|
383
|
+
|
|
384
|
+
# Check if tool is in the allowed list
|
|
385
|
+
for allowed in "${AGENT_TOOLS[@]}"; do
|
|
386
|
+
# Handle Task(Type) restriction syntax
|
|
387
|
+
local task_prefix_re='^Task\('
|
|
388
|
+
if [[ "$allowed" =~ $task_prefix_re ]]; then
|
|
389
|
+
if [[ "$tool" == "Task" ]]; then
|
|
390
|
+
return 0 # Has Task but with type restriction
|
|
391
|
+
fi
|
|
392
|
+
fi
|
|
393
|
+
if [[ "$allowed" == "$tool" ]]; then
|
|
394
|
+
return 0
|
|
395
|
+
fi
|
|
396
|
+
done
|
|
397
|
+
|
|
398
|
+
return 1
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
# Check if the agent's Task tool has a type restriction
|
|
402
|
+
# Returns the restriction type or empty string
|
|
403
|
+
get_task_restriction() {
|
|
404
|
+
local task_re='^Task\(([^)]+)\)$'
|
|
405
|
+
for tool in "${AGENT_TOOLS[@]}"; do
|
|
406
|
+
if [[ "$tool" =~ $task_re ]]; then
|
|
407
|
+
echo "${BASH_REMATCH[1]}"
|
|
408
|
+
return 0
|
|
409
|
+
fi
|
|
410
|
+
done
|
|
411
|
+
echo ""
|
|
412
|
+
return 0
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
# Get model tier (numeric)
|
|
416
|
+
get_model_tier() {
|
|
417
|
+
local model="$1"
|
|
418
|
+
echo "${MODEL_TIERS[$model]:-0}"
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
# Validate agent against requirements
|
|
422
|
+
# Returns 0 if compatible, 1 if not
|
|
423
|
+
validate_agent() {
|
|
424
|
+
local agent_file="$1"
|
|
425
|
+
local -a required_tools=()
|
|
426
|
+
local minimum_model=""
|
|
427
|
+
local -a required_skills=()
|
|
428
|
+
|
|
429
|
+
# Shift past agent_file
|
|
430
|
+
shift
|
|
431
|
+
|
|
432
|
+
# Parse remaining args
|
|
433
|
+
while [[ $# -gt 0 ]]; do
|
|
434
|
+
case "$1" in
|
|
435
|
+
--tools)
|
|
436
|
+
IFS=',' read -ra required_tools <<< "$2"
|
|
437
|
+
shift 2
|
|
438
|
+
;;
|
|
439
|
+
--model)
|
|
440
|
+
minimum_model="$2"
|
|
441
|
+
shift 2
|
|
442
|
+
;;
|
|
443
|
+
--skills)
|
|
444
|
+
IFS=',' read -ra required_skills <<< "$2"
|
|
445
|
+
shift 2
|
|
446
|
+
;;
|
|
447
|
+
*)
|
|
448
|
+
shift
|
|
449
|
+
;;
|
|
450
|
+
esac
|
|
451
|
+
done
|
|
452
|
+
|
|
453
|
+
# Parse agent frontmatter
|
|
454
|
+
if ! parse_agent_frontmatter "$agent_file"; then
|
|
455
|
+
return $EXIT_FILE_ERROR
|
|
456
|
+
fi
|
|
457
|
+
|
|
458
|
+
local agent_display="${AGENT_NAME:-$(basename "$agent_file" .md)}"
|
|
459
|
+
local has_failures=false
|
|
460
|
+
|
|
461
|
+
log_info "Checking agent: ${BOLD}${agent_display}${NC}"
|
|
462
|
+
log_info " File: $agent_file"
|
|
463
|
+
log_info " Model: ${AGENT_MODEL:-default}"
|
|
464
|
+
log_info " Tools: ${AGENT_TOOLS[*]:-all (no restrictions)}"
|
|
465
|
+
[[ ${#AGENT_DISALLOWED_TOOLS[@]} -gt 0 ]] && \
|
|
466
|
+
log_info " Disallowed: ${AGENT_DISALLOWED_TOOLS[*]}"
|
|
467
|
+
[[ ${#AGENT_SKILLS[@]} -gt 0 ]] && \
|
|
468
|
+
log_info " Skills: ${AGENT_SKILLS[*]}"
|
|
469
|
+
|
|
470
|
+
# --- Tool checks ---
|
|
471
|
+
if [[ ${#required_tools[@]} -gt 0 ]]; then
|
|
472
|
+
log_info "Checking required tools: ${required_tools[*]}"
|
|
473
|
+
for tool in "${required_tools[@]}"; do
|
|
474
|
+
tool="${tool#"${tool%%[![:space:]]*}"}"
|
|
475
|
+
tool="${tool%"${tool##*[![:space:]]}"}"
|
|
476
|
+
|
|
477
|
+
if agent_has_tool "$tool"; then
|
|
478
|
+
log_ok " Tool '$tool' available"
|
|
479
|
+
else
|
|
480
|
+
log_fail " Tool '$tool' MISSING - agent cannot perform this role"
|
|
481
|
+
has_failures=true
|
|
482
|
+
|
|
483
|
+
# Provide actionable details
|
|
484
|
+
local in_disallowed=false
|
|
485
|
+
for d in "${AGENT_DISALLOWED_TOOLS[@]}"; do
|
|
486
|
+
[[ "$d" == "$tool" ]] && in_disallowed=true
|
|
487
|
+
done
|
|
488
|
+
|
|
489
|
+
if [[ "$in_disallowed" == "true" ]]; then
|
|
490
|
+
log_fail " Reason: '$tool' is in disallowedTools"
|
|
491
|
+
elif [[ ${#AGENT_TOOLS[@]} -gt 0 ]]; then
|
|
492
|
+
log_fail " Reason: '$tool' not listed in tools (restricted agent)"
|
|
493
|
+
fi
|
|
494
|
+
fi
|
|
495
|
+
done
|
|
496
|
+
fi
|
|
497
|
+
|
|
498
|
+
# --- Task tool type restriction check ---
|
|
499
|
+
local task_restriction
|
|
500
|
+
task_restriction=$(get_task_restriction)
|
|
501
|
+
if [[ -n "$task_restriction" ]]; then
|
|
502
|
+
log_warn " Task tool restricted to type: $task_restriction"
|
|
503
|
+
# Check if any required tool is Task (without restriction)
|
|
504
|
+
for tool in "${required_tools[@]}"; do
|
|
505
|
+
if [[ "$tool" == "Task" ]]; then
|
|
506
|
+
log_warn " Role requires unrestricted Task but agent has Task($task_restriction)"
|
|
507
|
+
fi
|
|
508
|
+
done
|
|
509
|
+
fi
|
|
510
|
+
|
|
511
|
+
# --- Model tier check ---
|
|
512
|
+
if [[ -n "$minimum_model" ]]; then
|
|
513
|
+
log_info "Checking minimum model: $minimum_model"
|
|
514
|
+
local required_tier
|
|
515
|
+
required_tier=$(get_model_tier "$minimum_model")
|
|
516
|
+
local agent_model="${AGENT_MODEL:-sonnet}" # default to sonnet if unspecified
|
|
517
|
+
local agent_tier
|
|
518
|
+
agent_tier=$(get_model_tier "$agent_model")
|
|
519
|
+
|
|
520
|
+
if [[ $agent_tier -ge $required_tier ]]; then
|
|
521
|
+
log_ok " Model '${agent_model}' meets minimum '${minimum_model}' (tier ${agent_tier} >= ${required_tier})"
|
|
522
|
+
else
|
|
523
|
+
log_fail " Model '${agent_model}' below minimum '${minimum_model}' (tier ${agent_tier} < ${required_tier})"
|
|
524
|
+
has_failures=true
|
|
525
|
+
fi
|
|
526
|
+
fi
|
|
527
|
+
|
|
528
|
+
# --- Skills check ---
|
|
529
|
+
if [[ ${#required_skills[@]} -gt 0 ]]; then
|
|
530
|
+
log_info "Checking required skills: ${required_skills[*]}"
|
|
531
|
+
for skill in "${required_skills[@]}"; do
|
|
532
|
+
skill="${skill#"${skill%%[![:space:]]*}"}"
|
|
533
|
+
skill="${skill%"${skill##*[![:space:]]}"}"
|
|
534
|
+
|
|
535
|
+
local found=false
|
|
536
|
+
for agent_skill in "${AGENT_SKILLS[@]}"; do
|
|
537
|
+
if [[ "$agent_skill" == "$skill" ]]; then
|
|
538
|
+
found=true
|
|
539
|
+
break
|
|
540
|
+
fi
|
|
541
|
+
done
|
|
542
|
+
|
|
543
|
+
if [[ "$found" == "true" ]]; then
|
|
544
|
+
log_ok " Skill '$skill' available"
|
|
545
|
+
else
|
|
546
|
+
log_warn " Skill '$skill' not declared (may still work but not optimized)"
|
|
547
|
+
fi
|
|
548
|
+
done
|
|
549
|
+
fi
|
|
550
|
+
|
|
551
|
+
if [[ "$has_failures" == "true" ]]; then
|
|
552
|
+
return $EXIT_INCOMPATIBLE
|
|
553
|
+
fi
|
|
554
|
+
|
|
555
|
+
return $EXIT_OK
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
# =============================================================================
|
|
559
|
+
# JSON Output
|
|
560
|
+
# =============================================================================
|
|
561
|
+
|
|
562
|
+
output_json_result() {
|
|
563
|
+
local agent_file="$1"
|
|
564
|
+
local compatible="$2"
|
|
565
|
+
local -a missing_tools=("${@:3}")
|
|
566
|
+
|
|
567
|
+
local agent_display="${AGENT_NAME:-$(basename "$agent_file" .md)}"
|
|
568
|
+
|
|
569
|
+
printf '{\n'
|
|
570
|
+
printf ' "agent": "%s",\n' "$agent_display"
|
|
571
|
+
printf ' "file": "%s",\n' "$agent_file"
|
|
572
|
+
printf ' "model": "%s",\n' "${AGENT_MODEL:-default}"
|
|
573
|
+
printf ' "compatible": %s,\n' "$compatible"
|
|
574
|
+
|
|
575
|
+
# Tools array
|
|
576
|
+
printf ' "tools": ['
|
|
577
|
+
local first=true
|
|
578
|
+
for t in "${AGENT_TOOLS[@]}"; do
|
|
579
|
+
[[ "$first" == "true" ]] && first=false || printf ','
|
|
580
|
+
printf '"%s"' "$t"
|
|
581
|
+
done
|
|
582
|
+
printf '],\n'
|
|
583
|
+
|
|
584
|
+
# Missing tools
|
|
585
|
+
printf ' "missing_tools": ['
|
|
586
|
+
first=true
|
|
587
|
+
for t in "${missing_tools[@]}"; do
|
|
588
|
+
[[ "$first" == "true" ]] && first=false || printf ','
|
|
589
|
+
printf '"%s"' "$t"
|
|
590
|
+
done
|
|
591
|
+
printf ']\n'
|
|
592
|
+
|
|
593
|
+
printf '}\n'
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
# =============================================================================
|
|
597
|
+
# Dry Run
|
|
598
|
+
# =============================================================================
|
|
599
|
+
|
|
600
|
+
do_dry_run() {
|
|
601
|
+
local agent_file="$1"
|
|
602
|
+
local require_tools="$2"
|
|
603
|
+
local require_model="$3"
|
|
604
|
+
local require_skills="$4"
|
|
605
|
+
|
|
606
|
+
echo ""
|
|
607
|
+
echo "${BOLD}=== DRY RUN ===${NC}"
|
|
608
|
+
echo ""
|
|
609
|
+
echo "Would perform the following compatibility check:"
|
|
610
|
+
echo ""
|
|
611
|
+
|
|
612
|
+
if [[ -n "$agent_file" ]]; then
|
|
613
|
+
echo " Agent file: $agent_file"
|
|
614
|
+
if [[ -f "$agent_file" ]]; then
|
|
615
|
+
echo " File exists: yes"
|
|
616
|
+
# Parse for display only
|
|
617
|
+
if parse_agent_frontmatter "$agent_file" 2>/dev/null; then
|
|
618
|
+
echo " Agent name: ${AGENT_NAME:-<not set>}"
|
|
619
|
+
echo " Agent model: ${AGENT_MODEL:-<default>}"
|
|
620
|
+
echo " Agent tools: ${AGENT_TOOLS[*]:-<all, no restrictions>}"
|
|
621
|
+
[[ ${#AGENT_DISALLOWED_TOOLS[@]} -gt 0 ]] && \
|
|
622
|
+
echo " Disallowed: ${AGENT_DISALLOWED_TOOLS[*]}"
|
|
623
|
+
[[ ${#AGENT_SKILLS[@]} -gt 0 ]] && \
|
|
624
|
+
echo " Agent skills: ${AGENT_SKILLS[*]}"
|
|
625
|
+
else
|
|
626
|
+
echo " Parse status: FAILED (no frontmatter found)"
|
|
627
|
+
fi
|
|
628
|
+
else
|
|
629
|
+
echo " File exists: NO"
|
|
630
|
+
fi
|
|
631
|
+
fi
|
|
632
|
+
|
|
633
|
+
echo ""
|
|
634
|
+
echo " Requirements:"
|
|
635
|
+
[[ -n "$require_tools" ]] && echo " Tools: $require_tools"
|
|
636
|
+
[[ -n "$require_model" ]] && echo " Min model: $require_model"
|
|
637
|
+
[[ -n "$require_skills" ]] && echo " Skills: $require_skills"
|
|
638
|
+
echo ""
|
|
639
|
+
echo "No validation performed (dry run)."
|
|
640
|
+
echo ""
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
# =============================================================================
|
|
644
|
+
# Batch Mode
|
|
645
|
+
# =============================================================================
|
|
646
|
+
|
|
647
|
+
do_batch_check() {
|
|
648
|
+
local agent_dir="$1"
|
|
649
|
+
local require_tools="$2"
|
|
650
|
+
local require_model="$3"
|
|
651
|
+
local require_skills="$4"
|
|
652
|
+
local use_json="${5:-false}"
|
|
653
|
+
|
|
654
|
+
if [[ ! -d "$agent_dir" ]]; then
|
|
655
|
+
log_error "Agent directory not found: $agent_dir"
|
|
656
|
+
return $EXIT_FILE_ERROR
|
|
657
|
+
fi
|
|
658
|
+
|
|
659
|
+
local total=0
|
|
660
|
+
local passed=0
|
|
661
|
+
local failed=0
|
|
662
|
+
local errors=0
|
|
663
|
+
local -a failed_agents=()
|
|
664
|
+
|
|
665
|
+
[[ "$use_json" == "true" ]] && printf '[\n'
|
|
666
|
+
local first_json=true
|
|
667
|
+
|
|
668
|
+
for agent_file in "$agent_dir"/*.md; do
|
|
669
|
+
[[ ! -f "$agent_file" ]] && continue
|
|
670
|
+
total=$((total + 1))
|
|
671
|
+
|
|
672
|
+
local args=()
|
|
673
|
+
[[ -n "$require_tools" ]] && args+=(--tools "$require_tools")
|
|
674
|
+
[[ -n "$require_model" ]] && args+=(--model "$require_model")
|
|
675
|
+
[[ -n "$require_skills" ]] && args+=(--skills "$require_skills")
|
|
676
|
+
|
|
677
|
+
if validate_agent "$agent_file" "${args[@]}" 2>/dev/null; then
|
|
678
|
+
passed=$((passed + 1))
|
|
679
|
+
else
|
|
680
|
+
local rc=$?
|
|
681
|
+
if [[ $rc -eq $EXIT_FILE_ERROR ]]; then
|
|
682
|
+
errors=$((errors + 1))
|
|
683
|
+
else
|
|
684
|
+
failed=$((failed + 1))
|
|
685
|
+
failed_agents+=("$(basename "$agent_file" .md)")
|
|
686
|
+
fi
|
|
687
|
+
fi
|
|
688
|
+
|
|
689
|
+
echo "" # Separator between agents
|
|
690
|
+
done
|
|
691
|
+
|
|
692
|
+
[[ "$use_json" == "true" ]] && printf ']\n'
|
|
693
|
+
|
|
694
|
+
# Summary
|
|
695
|
+
echo "${BOLD}========================================${NC}"
|
|
696
|
+
echo "${BOLD}Batch Compatibility Summary${NC}"
|
|
697
|
+
echo "${BOLD}========================================${NC}"
|
|
698
|
+
echo " Directory: $agent_dir"
|
|
699
|
+
echo " Total agents: $total"
|
|
700
|
+
printf " ${GREEN}Compatible: %d${NC}\n" "$passed"
|
|
701
|
+
[[ $failed -gt 0 ]] && printf " ${RED}Incompatible: %d${NC}\n" "$failed"
|
|
702
|
+
[[ $errors -gt 0 ]] && printf " ${YELLOW}Parse errors: %d${NC}\n" "$errors"
|
|
703
|
+
|
|
704
|
+
if [[ ${#failed_agents[@]} -gt 0 ]]; then
|
|
705
|
+
echo ""
|
|
706
|
+
echo " Incompatible agents:"
|
|
707
|
+
for agent in "${failed_agents[@]}"; do
|
|
708
|
+
printf " ${RED}- %s${NC}\n" "$agent"
|
|
709
|
+
done
|
|
710
|
+
fi
|
|
711
|
+
|
|
712
|
+
echo "${BOLD}========================================${NC}"
|
|
713
|
+
|
|
714
|
+
[[ $failed -gt 0 || $errors -gt 0 ]] && return $EXIT_INCOMPATIBLE
|
|
715
|
+
return $EXIT_OK
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
# =============================================================================
|
|
719
|
+
# Main
|
|
720
|
+
# =============================================================================
|
|
721
|
+
|
|
722
|
+
main() {
|
|
723
|
+
local agent_file=""
|
|
724
|
+
local agent_dir=""
|
|
725
|
+
local require_tools=""
|
|
726
|
+
local require_model=""
|
|
727
|
+
local require_skills=""
|
|
728
|
+
local role_file=""
|
|
729
|
+
local batch=false
|
|
730
|
+
local dry_run=false
|
|
731
|
+
local use_json=false
|
|
732
|
+
QUIET=false
|
|
733
|
+
|
|
734
|
+
# Parse arguments
|
|
735
|
+
while [[ $# -gt 0 ]]; do
|
|
736
|
+
case "$1" in
|
|
737
|
+
-a|--agent)
|
|
738
|
+
agent_file="$2"
|
|
739
|
+
shift 2
|
|
740
|
+
;;
|
|
741
|
+
-d|--agent-dir)
|
|
742
|
+
agent_dir="$2"
|
|
743
|
+
shift 2
|
|
744
|
+
;;
|
|
745
|
+
-t|--require-tools)
|
|
746
|
+
require_tools="$2"
|
|
747
|
+
shift 2
|
|
748
|
+
;;
|
|
749
|
+
-m|--require-model)
|
|
750
|
+
require_model="$2"
|
|
751
|
+
shift 2
|
|
752
|
+
;;
|
|
753
|
+
-s|--require-skills)
|
|
754
|
+
require_skills="$2"
|
|
755
|
+
shift 2
|
|
756
|
+
;;
|
|
757
|
+
-r|--role-file)
|
|
758
|
+
role_file="$2"
|
|
759
|
+
shift 2
|
|
760
|
+
;;
|
|
761
|
+
-b|--batch)
|
|
762
|
+
batch=true
|
|
763
|
+
shift
|
|
764
|
+
;;
|
|
765
|
+
-n|--dry-run)
|
|
766
|
+
dry_run=true
|
|
767
|
+
shift
|
|
768
|
+
;;
|
|
769
|
+
-q|--quiet)
|
|
770
|
+
QUIET=true
|
|
771
|
+
shift
|
|
772
|
+
;;
|
|
773
|
+
-j|--json)
|
|
774
|
+
use_json=true
|
|
775
|
+
shift
|
|
776
|
+
;;
|
|
777
|
+
-h|--help)
|
|
778
|
+
show_help
|
|
779
|
+
exit $EXIT_OK
|
|
780
|
+
;;
|
|
781
|
+
-v|--version)
|
|
782
|
+
show_version
|
|
783
|
+
exit $EXIT_OK
|
|
784
|
+
;;
|
|
785
|
+
*)
|
|
786
|
+
log_error "Unknown option: $1"
|
|
787
|
+
echo "Use --help for usage information." >&2
|
|
788
|
+
exit $EXIT_USAGE
|
|
789
|
+
;;
|
|
790
|
+
esac
|
|
791
|
+
done
|
|
792
|
+
|
|
793
|
+
# Load role file requirements if provided
|
|
794
|
+
if [[ -n "$role_file" ]]; then
|
|
795
|
+
if ! parse_role_file "$role_file"; then
|
|
796
|
+
exit $EXIT_FILE_ERROR
|
|
797
|
+
fi
|
|
798
|
+
# Merge role file requirements with CLI requirements
|
|
799
|
+
if [[ ${#ROLE_REQUIRED_TOOLS[@]} -gt 0 && -z "$require_tools" ]]; then
|
|
800
|
+
require_tools=$(IFS=','; echo "${ROLE_REQUIRED_TOOLS[*]}")
|
|
801
|
+
fi
|
|
802
|
+
if [[ -n "$ROLE_MINIMUM_MODEL" && -z "$require_model" ]]; then
|
|
803
|
+
require_model="$ROLE_MINIMUM_MODEL"
|
|
804
|
+
fi
|
|
805
|
+
if [[ ${#ROLE_REQUIRED_SKILLS[@]} -gt 0 && -z "$require_skills" ]]; then
|
|
806
|
+
require_skills=$(IFS=','; echo "${ROLE_REQUIRED_SKILLS[*]}")
|
|
807
|
+
fi
|
|
808
|
+
fi
|
|
809
|
+
|
|
810
|
+
# Validate inputs
|
|
811
|
+
if [[ -z "$agent_file" && -z "$agent_dir" ]]; then
|
|
812
|
+
log_error "Must specify --agent <file> or --agent-dir <dir>"
|
|
813
|
+
echo "Use --help for usage information." >&2
|
|
814
|
+
exit $EXIT_USAGE
|
|
815
|
+
fi
|
|
816
|
+
|
|
817
|
+
if [[ -z "$require_tools" && -z "$require_model" && -z "$require_skills" ]]; then
|
|
818
|
+
log_error "Must specify at least one requirement: --require-tools, --require-model, or --require-skills"
|
|
819
|
+
echo "Use --help for usage information." >&2
|
|
820
|
+
exit $EXIT_USAGE
|
|
821
|
+
fi
|
|
822
|
+
|
|
823
|
+
# Dry run mode
|
|
824
|
+
if [[ "$dry_run" == "true" ]]; then
|
|
825
|
+
if [[ "$batch" == "true" && -n "$agent_dir" ]]; then
|
|
826
|
+
echo ""
|
|
827
|
+
echo "${BOLD}=== DRY RUN (Batch) ===${NC}"
|
|
828
|
+
echo ""
|
|
829
|
+
echo "Would check all .md files in: $agent_dir"
|
|
830
|
+
local count=0
|
|
831
|
+
for f in "$agent_dir"/*.md; do
|
|
832
|
+
[[ -f "$f" ]] && count=$((count + 1)) && echo " - $(basename "$f")"
|
|
833
|
+
done
|
|
834
|
+
echo ""
|
|
835
|
+
echo " Total files: $count"
|
|
836
|
+
echo " Requirements:"
|
|
837
|
+
[[ -n "$require_tools" ]] && echo " Tools: $require_tools"
|
|
838
|
+
[[ -n "$require_model" ]] && echo " Min model: $require_model"
|
|
839
|
+
[[ -n "$require_skills" ]] && echo " Skills: $require_skills"
|
|
840
|
+
echo ""
|
|
841
|
+
echo "No validation performed (dry run)."
|
|
842
|
+
else
|
|
843
|
+
do_dry_run "$agent_file" "$require_tools" "$require_model" "$require_skills"
|
|
844
|
+
fi
|
|
845
|
+
exit $EXIT_OK
|
|
846
|
+
fi
|
|
847
|
+
|
|
848
|
+
# Batch mode
|
|
849
|
+
if [[ "$batch" == "true" ]]; then
|
|
850
|
+
if [[ -z "$agent_dir" ]]; then
|
|
851
|
+
log_error "--batch requires --agent-dir"
|
|
852
|
+
exit $EXIT_USAGE
|
|
853
|
+
fi
|
|
854
|
+
do_batch_check "$agent_dir" "$require_tools" "$require_model" "$require_skills" "$use_json"
|
|
855
|
+
exit $?
|
|
856
|
+
fi
|
|
857
|
+
|
|
858
|
+
# Single agent check
|
|
859
|
+
local args=()
|
|
860
|
+
[[ -n "$require_tools" ]] && args+=(--tools "$require_tools")
|
|
861
|
+
[[ -n "$require_model" ]] && args+=(--model "$require_model")
|
|
862
|
+
[[ -n "$require_skills" ]] && args+=(--skills "$require_skills")
|
|
863
|
+
|
|
864
|
+
if validate_agent "$agent_file" "${args[@]}"; then
|
|
865
|
+
echo ""
|
|
866
|
+
log_ok "${BOLD}Agent is COMPATIBLE with role requirements${NC}"
|
|
867
|
+
exit $EXIT_OK
|
|
868
|
+
else
|
|
869
|
+
local rc=$?
|
|
870
|
+
echo ""
|
|
871
|
+
if [[ $rc -eq $EXIT_FILE_ERROR ]]; then
|
|
872
|
+
log_error "Could not parse agent file"
|
|
873
|
+
else
|
|
874
|
+
log_fail "${BOLD}Agent is INCOMPATIBLE with role requirements${NC}"
|
|
875
|
+
fi
|
|
876
|
+
exit $rc
|
|
877
|
+
fi
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
# Run only if executed directly (not sourced)
|
|
881
|
+
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
882
|
+
main "$@"
|
|
883
|
+
fi
|