@the-bearded-bear/claude-craft 5.6.0-next.8bfdffa → 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.
@@ -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