ai-consultants 2.10.1

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.
Files changed (62) hide show
  1. package/.claude/commands/ai-consultants:consult.md +94 -0
  2. package/.claude/commands/ai-consultants:debate.md +103 -0
  3. package/.claude/commands/ai-consultants:help.md +30 -0
  4. package/CLAUDE.md +839 -0
  5. package/LICENSE +21 -0
  6. package/README.md +758 -0
  7. package/SKILL.md +156 -0
  8. package/bin/ai-consultants +96 -0
  9. package/docs/COST_RATES.md +172 -0
  10. package/docs/JSON_SCHEMA.md +305 -0
  11. package/docs/SETUP.md +879 -0
  12. package/docs/SMART_ROUTING.md +219 -0
  13. package/docs/cost_rates.json +143 -0
  14. package/package.json +47 -0
  15. package/references/configuration.md +141 -0
  16. package/references/details.md +60 -0
  17. package/scripts/build_context.sh +562 -0
  18. package/scripts/classify_question.sh +148 -0
  19. package/scripts/config.sh +753 -0
  20. package/scripts/configure.sh +1093 -0
  21. package/scripts/consult_all.sh +934 -0
  22. package/scripts/debate_round.sh +421 -0
  23. package/scripts/doctor.sh +642 -0
  24. package/scripts/followup.sh +197 -0
  25. package/scripts/install.sh +319 -0
  26. package/scripts/lib/api.sh +640 -0
  27. package/scripts/lib/api_query.sh +228 -0
  28. package/scripts/lib/cache.sh +338 -0
  29. package/scripts/lib/chunking.sh +880 -0
  30. package/scripts/lib/code_optimizer.sh +671 -0
  31. package/scripts/lib/common.sh +1088 -0
  32. package/scripts/lib/costs.sh +674 -0
  33. package/scripts/lib/personas.sh +372 -0
  34. package/scripts/lib/progress.sh +251 -0
  35. package/scripts/lib/reflection.sh +447 -0
  36. package/scripts/lib/routing.sh +566 -0
  37. package/scripts/lib/schema.json +165 -0
  38. package/scripts/lib/session.sh +327 -0
  39. package/scripts/lib/symbol_map.sh +445 -0
  40. package/scripts/lib/voting.sh +495 -0
  41. package/scripts/peer_review.sh +460 -0
  42. package/scripts/preflight_check.sh +452 -0
  43. package/scripts/query_aider.sh +83 -0
  44. package/scripts/query_amp.sh +67 -0
  45. package/scripts/query_claude.sh +99 -0
  46. package/scripts/query_codex.sh +92 -0
  47. package/scripts/query_cursor.sh +72 -0
  48. package/scripts/query_deepseek.sh +17 -0
  49. package/scripts/query_gemini.sh +85 -0
  50. package/scripts/query_glm.sh +17 -0
  51. package/scripts/query_grok.sh +17 -0
  52. package/scripts/query_kilo.sh +140 -0
  53. package/scripts/query_kimi.sh +67 -0
  54. package/scripts/query_minimax.sh +17 -0
  55. package/scripts/query_mistral.sh +97 -0
  56. package/scripts/query_ollama.sh +328 -0
  57. package/scripts/query_qwen3.sh +93 -0
  58. package/scripts/setup_wizard.sh +367 -0
  59. package/scripts/skillport-install.sh +196 -0
  60. package/scripts/synthesize.sh +332 -0
  61. package/templates/consultation_report.md +323 -0
  62. package/templates/synthesis_prompt.md +120 -0
@@ -0,0 +1,934 @@
1
+ #!/bin/bash
2
+ # consult_all.sh - AI Consultants - Main Orchestrator
3
+ #
4
+ # Complete workflow for multi-model AI consultation with:
5
+ # - Specialized personas for each consultant
6
+ # - Confidence scoring and weighted voting
7
+ # - Auto-synthesis of responses with multiple strategies
8
+ # - Optional Multi-Agent Debate (MAD)
9
+ # - Smart routing based on question category
10
+ # - Configuration presets for quick setup
11
+ # - Session management for follow-up
12
+ # - Cost tracking
13
+ # - Panic button mode for uncertainty detection
14
+ #
15
+ # Usage: ./consult_all.sh [options] "Your question" [file1] [file2] ...
16
+ #
17
+ # Options:
18
+ # --preset <name> Use a configuration preset (minimal, balanced, thorough, high-stakes, local, security, cost-capped)
19
+ # --strategy <name> Synthesis strategy (majority, risk_averse, security_first, cost_capped, compare_only)
20
+ # --list-presets List available presets
21
+ # --list-strategies List available synthesis strategies
22
+ # --help Show this help message
23
+ #
24
+ # Environment variables:
25
+ # ENABLE_SYNTHESIS=true Enable automatic synthesis
26
+ # ENABLE_DEBATE=true Enable multi-round debate
27
+ # DEBATE_ROUNDS=2 Number of debate rounds
28
+ # ENABLE_SMART_ROUTING=true Select consultants based on question
29
+ # ENABLE_PANIC_MODE=auto Panic mode (auto, always, never)
30
+
31
+ set -euo pipefail
32
+
33
+ # --- Initial Setup ---
34
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
35
+ source "$SCRIPT_DIR/lib/common.sh"
36
+ source "$SCRIPT_DIR/lib/session.sh"
37
+ source "$SCRIPT_DIR/lib/progress.sh"
38
+ source "$SCRIPT_DIR/lib/costs.sh"
39
+ source "$SCRIPT_DIR/lib/voting.sh"
40
+ source "$SCRIPT_DIR/lib/routing.sh"
41
+ source "$SCRIPT_DIR/lib/cache.sh"
42
+
43
+ # --- Custom API Agent Discovery ---
44
+ # Discovers custom API agents from environment variables
45
+ # Convention: ENABLE_AGENTNAME=true with AGENTNAME_API_URL set
46
+ _discover_custom_api_agents() {
47
+ while IFS='=' read -r var value; do
48
+ [[ "$var" != ENABLE_* ]] && continue
49
+ [[ "$value" != "true" ]] && continue
50
+
51
+ local agent_upper="${var#ENABLE_}"
52
+
53
+ # Skip known agents (uses helper from common.sh)
54
+ is_known_agent "$agent_upper" && continue
55
+
56
+ # Check if it has an API URL configured (indicates it's an API agent)
57
+ local url_var="${agent_upper}_API_URL"
58
+ [[ -z "${!url_var:-}" ]] && continue
59
+
60
+ # Add custom API agent with proper case
61
+ local agent_name
62
+ agent_name=$(to_title "$agent_upper")
63
+ SELECTED_CONSULTANTS+=("$agent_name")
64
+ log_debug "Discovered custom API agent: $agent_name"
65
+ done < <(env)
66
+ }
67
+
68
+ # --- Show usage help ---
69
+ show_help() {
70
+ echo "AI Consultants v${AI_CONSULTANTS_VERSION:-2.10.0} - Multi-Model AI Consultation"
71
+ cat << 'EOF'
72
+
73
+ Usage: ./consult_all.sh [options] "Your question" [file1] [file2] ...
74
+
75
+ Options:
76
+ --preset <name> Use a configuration preset:
77
+ minimal - 2 models, fast and cheap
78
+ balanced - 4 models, good coverage
79
+ thorough - 5 models, comprehensive
80
+ high-stakes - All models + debate
81
+ max_quality - All + premium models + debate + reflection
82
+ medium - 4 models + standard models + light debate
83
+ fast - 2 models + economy models, no debate
84
+ local - Ollama only, full privacy
85
+ security - Security-focused + debate
86
+ cost-capped - Budget-conscious options
87
+
88
+ --strategy <name> Synthesis strategy:
89
+ majority - Simple voting, most common wins
90
+ risk_averse - Weight conservative responses higher
91
+ security_first - Prioritize security-focused insights
92
+ cost_capped - Prefer cheaper consultant opinions
93
+ compare_only - No recommendation, just comparison
94
+
95
+ --list-presets List all available presets
96
+ --list-strategies List all synthesis strategies
97
+ --help, -h Show this help message
98
+
99
+ Examples:
100
+ ./consult_all.sh "How to optimize this SQL query?"
101
+ ./consult_all.sh --preset minimal "Quick question about Python lists"
102
+ ./consult_all.sh --preset high-stakes --strategy risk_averse "Critical architecture decision"
103
+ ./consult_all.sh "Review this code" src/main.py src/utils.py
104
+
105
+ Environment Variables:
106
+ ENABLE_SYNTHESIS=true Enable automatic synthesis
107
+ ENABLE_DEBATE=true Enable multi-round debate
108
+ DEBATE_ROUNDS=2 Number of debate rounds
109
+ ENABLE_SMART_ROUTING=true Select consultants based on question category
110
+ ENABLE_PANIC_MODE=auto Panic mode: auto, always, never
111
+ MAX_SESSION_COST=1.00 Maximum budget per session ($)
112
+
113
+ For more information, run: ./doctor.sh
114
+ EOF
115
+ }
116
+
117
+ # --- List synthesis strategies ---
118
+ list_strategies() {
119
+ cat << 'EOF'
120
+ Available synthesis strategies:
121
+
122
+ majority Simple voting - most common answer wins (default)
123
+ risk_averse Weight conservative responses higher, prefer safety
124
+ security_first Prioritize security-focused consultants and insights
125
+ cost_capped Prefer opinions from cheaper consultants within budget
126
+ compare_only No recommendation, just structured comparison table
127
+
128
+ Usage: ./consult_all.sh --strategy <name> "Your question"
129
+ EOF
130
+ }
131
+
132
+ # --- Argument Parsing ---
133
+ PRESET=""
134
+ SYNTHESIS_STRATEGY=""
135
+ QUERY=""
136
+ FILES=()
137
+
138
+ while [[ $# -gt 0 ]]; do
139
+ case "$1" in
140
+ --preset)
141
+ if [[ -n "${2:-}" ]]; then
142
+ PRESET="$2"
143
+ shift 2
144
+ else
145
+ log_error "--preset requires a value"
146
+ exit 1
147
+ fi
148
+ ;;
149
+ --strategy)
150
+ if [[ -n "${2:-}" ]]; then
151
+ SYNTHESIS_STRATEGY="$2"
152
+ shift 2
153
+ else
154
+ log_error "--strategy requires a value"
155
+ exit 1
156
+ fi
157
+ ;;
158
+ --list-presets)
159
+ list_presets
160
+ exit 0
161
+ ;;
162
+ --list-strategies)
163
+ list_strategies
164
+ exit 0
165
+ ;;
166
+ --help|-h)
167
+ show_help
168
+ exit 0
169
+ ;;
170
+ -*)
171
+ log_error "Unknown option: $1"
172
+ show_help
173
+ exit 1
174
+ ;;
175
+ *)
176
+ if [[ -z "$QUERY" ]]; then
177
+ QUERY="$1"
178
+ else
179
+ FILES+=("$1")
180
+ fi
181
+ shift
182
+ ;;
183
+ esac
184
+ done
185
+
186
+ # --- Apply Preset (CLI flag or default) ---
187
+ # Use CLI flag if provided, otherwise fall back to DEFAULT_PRESET
188
+ PRESET="${PRESET:-${DEFAULT_PRESET:-}}"
189
+ if [[ -n "$PRESET" ]]; then
190
+ if ! apply_preset "$PRESET"; then
191
+ exit 1
192
+ fi
193
+ log_info "Applied preset: $PRESET"
194
+ fi
195
+
196
+ # Set synthesis strategy: CLI flag > environment > DEFAULT_STRATEGY > fallback
197
+ SYNTHESIS_STRATEGY="${SYNTHESIS_STRATEGY:-${DEFAULT_STRATEGY:-majority}}"
198
+
199
+ # Export synthesis strategy for use by synthesize.sh
200
+ export SYNTHESIS_STRATEGY
201
+
202
+ # --- Input Validation ---
203
+ if [[ -z "$QUERY" ]]; then
204
+ log_error "Usage: $0 [options] \"Your question\" [file1] [file2] ..."
205
+ log_info "Example: $0 \"How to optimize this function?\" src/utils.py"
206
+ log_info "Run with --help for more options"
207
+ exit 1
208
+ fi
209
+
210
+ # --- Header ---
211
+ echo "" >&2
212
+ echo "╔══════════════════════════════════════════════════════════════╗" >&2
213
+ echo "║ AI Consultants v${AI_CONSULTANTS_VERSION} - Expert Panel ║" >&2
214
+ echo "╚══════════════════════════════════════════════════════════════╝" >&2
215
+ echo "" >&2
216
+
217
+ # --- Pre-flight Check (optional) ---
218
+ if [[ "$ENABLE_PREFLIGHT" == "true" ]]; then
219
+ log_info "Running pre-flight check..."
220
+ preflight_args=()
221
+ [[ "$PREFLIGHT_QUICK" == "true" ]] && preflight_args+=("--quick")
222
+ if ! "$SCRIPT_DIR/preflight_check.sh" ${preflight_args[@]+"${preflight_args[@]}"} > /dev/null 2>&1; then
223
+ log_error "Pre-flight check failed. Run ./preflight_check.sh for details."
224
+ exit 1
225
+ fi
226
+ log_success "Pre-flight check passed"
227
+ echo "" >&2
228
+ fi
229
+
230
+ # --- Question Classification ---
231
+ QUESTION_CATEGORY="GENERAL"
232
+ if [[ "$ENABLE_CLASSIFICATION" == "true" ]]; then
233
+ log_info "Classifying question..."
234
+ QUESTION_CATEGORY=$("$SCRIPT_DIR/classify_question.sh" "$QUERY" 2>/dev/null || echo "GENERAL")
235
+ log_info "Category: $QUESTION_CATEGORY"
236
+ fi
237
+
238
+ # --- Create Output Directory (with secure permissions) ---
239
+ # Add random suffix to prevent predictable directory names (security improvement)
240
+ TIMESTAMP=$(date +%Y%m%d_%H%M%S)
241
+ RANDOM_SUFFIX="${RANDOM}${$}"
242
+ OUTPUT_DIR="${DEFAULT_OUTPUT_DIR_BASE}/${TIMESTAMP}_${RANDOM_SUFFIX}"
243
+ # Create base directory with restricted permissions (owner only)
244
+ mkdir -p "$DEFAULT_OUTPUT_DIR_BASE"
245
+ chmod 700 "$DEFAULT_OUTPUT_DIR_BASE"
246
+ # Create session-specific directory
247
+ mkdir -p "$OUTPUT_DIR"
248
+ chmod 700 "$OUTPUT_DIR"
249
+
250
+ log_info "Output: $OUTPUT_DIR"
251
+ log_info "Question: ${QUERY:0:80}$([ ${#QUERY} -gt 80 ] && echo '...')"
252
+ if [[ ${#FILES[@]} -gt 0 ]]; then
253
+ log_info "Files: ${FILES[*]}"
254
+ fi
255
+ echo "" >&2
256
+
257
+ # --- Automatic Context Creation ---
258
+ log_info "Building automatic context..."
259
+ CONTEXT_FILE="$OUTPUT_DIR/context.md"
260
+ if [[ ${#FILES[@]} -gt 0 ]]; then
261
+ "$SCRIPT_DIR/build_context.sh" "$CONTEXT_FILE" "$QUERY" "${FILES[@]}" > /dev/null
262
+ else
263
+ "$SCRIPT_DIR/build_context.sh" "$CONTEXT_FILE" "$QUERY" > /dev/null
264
+ fi
265
+ log_success "Context created: $CONTEXT_FILE ($(wc -l < "$CONTEXT_FILE" | tr -d ' ') lines)"
266
+
267
+ # --- Cost Estimation (optional) ---
268
+ CONTEXT_SIZE=0
269
+ ESTIMATED_COST=0
270
+ if [[ "$ENABLE_COST_TRACKING" == "true" ]]; then
271
+ CONTEXT_SIZE=$(wc -c < "$CONTEXT_FILE" | tr -d ' ')
272
+ ESTIMATED_COST=$(estimate_consultation_cost 4 "$CONTEXT_SIZE")
273
+ log_info "Estimated cost: $(format_cost "$ESTIMATED_COST")"
274
+ fi
275
+
276
+ # --- Budget Check: Before Round 1 ---
277
+ if is_budget_enabled; then
278
+ log_debug "Budget enforcement enabled: \$${MAX_SESSION_COST} limit"
279
+ if ! enforce_budget 0 "$ESTIMATED_COST" "initial consultation"; then
280
+ log_error "Consultation aborted: estimated cost exceeds budget"
281
+ exit 1
282
+ fi
283
+ fi
284
+ echo "" >&2
285
+
286
+ # --- Consultant Selection ---
287
+ declare -a SELECTED_CONSULTANTS=()
288
+
289
+ if [[ "$ENABLE_SMART_ROUTING" == "true" ]]; then
290
+ log_info "Smart routing for category $QUESTION_CATEGORY..."
291
+ while IFS= read -r c; do
292
+ [[ -n "$c" ]] && SELECTED_CONSULTANTS+=("$c")
293
+ done < <(select_consultants "$QUESTION_CATEGORY" "$MIN_AFFINITY")
294
+ log_info "Selected consultants: ${SELECTED_CONSULTANTS[*]}"
295
+ else
296
+ # Log self-exclusion status for debugging
297
+ log_self_exclusion_status
298
+
299
+ # All enabled consultants - use a compact loop
300
+ # Order matches ALL_CONSULTANTS in config.sh for consistency
301
+ _consultant_map="GEMINI:Gemini CODEX:Codex MISTRAL:Mistral KILO:Kilo CURSOR:Cursor AIDER:Aider AMP:Amp KIMI:Kimi CLAUDE:Claude QWEN3:Qwen3 GLM:GLM GROK:Grok DEEPSEEK:DeepSeek MINIMAX:MiniMax OLLAMA:Ollama"
302
+ for _entry in $_consultant_map; do
303
+ _flag="${_entry%%:*}"
304
+ _name="${_entry#*:}"
305
+ _enable_var="ENABLE_${_flag}"
306
+
307
+ # Skip self (invoking agent shouldn't query itself)
308
+ if should_skip_consultant "$_flag"; then
309
+ log_debug "Skipping $_name (self-exclusion: invoking agent)"
310
+ continue
311
+ fi
312
+
313
+ [[ "${!_enable_var:-false}" == "true" ]] && SELECTED_CONSULTANTS+=("$_name")
314
+ done
315
+ unset _consultant_map _entry _flag _name _enable_var
316
+
317
+ # Discover custom API agents from environment
318
+ _discover_custom_api_agents
319
+ fi
320
+
321
+ if [[ ${#SELECTED_CONSULTANTS[@]} -eq 0 ]]; then
322
+ log_error "No consultant enabled or selected"
323
+ exit 1
324
+ fi
325
+
326
+ # Minimum 2 consultants required for meaningful comparison
327
+ if [[ ${#SELECTED_CONSULTANTS[@]} -lt 2 ]]; then
328
+ log_error "At least 2 consultants required for meaningful comparison."
329
+ log_error "Currently enabled: ${#SELECTED_CONSULTANTS[@]} (${SELECTED_CONSULTANTS[*]})"
330
+ echo "" >&2
331
+ log_info "Run ./scripts/setup_wizard.sh to configure additional consultants"
332
+ log_info "Or manually enable more:"
333
+ log_info " ENABLE_GEMINI=true ENABLE_CODEX=true ./scripts/consult_all.sh \"query\""
334
+ exit 1
335
+ fi
336
+
337
+ # --- Refined Cost Estimation (v2.4) ---
338
+ # Now that we know which consultants are selected, recalculate with accurate model rates
339
+ if [[ "$ENABLE_COST_TRACKING" == "true" ]]; then
340
+ CONSULTANT_LIST=$(IFS=','; echo "${SELECTED_CONSULTANTS[*]}")
341
+ ESTIMATED_COST=$(estimate_consultation_cost "${#SELECTED_CONSULTANTS[@]}" "$CONTEXT_SIZE" "$CONSULTANT_LIST")
342
+ log_debug "Refined estimate with ${#SELECTED_CONSULTANTS[@]} consultants: $(format_cost "$ESTIMATED_COST")"
343
+
344
+ # Re-check budget with refined estimate
345
+ if is_budget_enabled; then
346
+ if ! enforce_budget 0 "$ESTIMATED_COST" "refined consultation estimate"; then
347
+ log_error "Consultation aborted: refined estimate exceeds budget"
348
+ exit 1
349
+ fi
350
+ fi
351
+ fi
352
+
353
+ # --- Round 1: Parallel Consultation ---
354
+ log_info "Starting parallel consultation (Round 1)..."
355
+ echo "" >&2
356
+
357
+ declare -a PIDS=()
358
+ declare -a NAMES=()
359
+ declare -a OUTPUT_FILES=()
360
+
361
+ # Initialize progress for each consultant
362
+ for c in "${SELECTED_CONSULTANTS[@]}"; do
363
+ init_progress "$c"
364
+ done
365
+
366
+ # Track cache hits
367
+ declare -a CACHE_HITS=()
368
+
369
+ # Launch consultants in parallel (with cache check)
370
+ for consultant in "${SELECTED_CONSULTANTS[@]}"; do
371
+ consultant_lower=$(to_lower "$consultant")
372
+ local_output_file="$OUTPUT_DIR/${consultant_lower}.json"
373
+ OUTPUT_FILES+=("$local_output_file")
374
+ NAMES+=("$consultant")
375
+
376
+ # Check cache first (v2.3)
377
+ if is_cache_enabled; then
378
+ cached_response=$(check_cache "$QUERY" "$QUESTION_CATEGORY" "$consultant_lower" "$CONTEXT_FILE" 2>/dev/null || echo "")
379
+ if [[ -n "$cached_response" ]]; then
380
+ # Use cached response
381
+ echo "$cached_response" | mark_from_cache > "$local_output_file"
382
+ PIDS+=("cache") # Marker for cache hit
383
+ CACHE_HITS+=("$consultant")
384
+ update_progress "$consultant" 100 "cached"
385
+ log_debug "Cache hit for $consultant"
386
+ continue
387
+ fi
388
+ fi
389
+
390
+ # Convention: query script is query_<lowercase_name>.sh
391
+ query_script="$SCRIPT_DIR/query_${consultant_lower}.sh"
392
+
393
+ if [[ -x "$query_script" ]]; then
394
+ # Apply cost-aware model override if enabled (v2.3)
395
+ if [[ "${ENABLE_COST_AWARE_ROUTING:-false}" == "true" ]] && type get_cost_aware_model &>/dev/null; then
396
+ local_model=$(get_cost_aware_model "$consultant_lower" "${QUERY_COMPLEXITY:-5}" 2>/dev/null || echo "")
397
+ if [[ -n "$local_model" ]]; then
398
+ model_var="$(echo "${consultant_lower}" | tr '[:lower:]' '[:upper:]')_MODEL"
399
+ if [[ ! "$model_var" =~ ^[A-Z0-9_]+_MODEL$ ]]; then
400
+ log_warn "Invalid model variable name: $model_var, skipping cost-aware override"
401
+ continue
402
+ fi
403
+ log_debug "Cost-aware: $consultant using model $local_model"
404
+ (
405
+ apply_launch_stagger
406
+ export "$model_var=$local_model"
407
+ "$query_script" "" "$CONTEXT_FILE" "$local_output_file"
408
+ ) > /dev/null 2>&1 &
409
+ PIDS+=($!)
410
+ update_progress "$consultant" 10 "running"
411
+ continue
412
+ fi
413
+ fi
414
+ # Standard launch (inherits current environment models)
415
+ ( apply_launch_stagger; "$query_script" "" "$CONTEXT_FILE" "$local_output_file" ) > /dev/null 2>&1 &
416
+ else
417
+ # Fallback: custom API agent via generic API query
418
+ (
419
+ apply_launch_stagger
420
+ source "$SCRIPT_DIR/lib/api_query.sh"
421
+ run_api_consultant "$consultant" "" "$CONTEXT_FILE" "$local_output_file"
422
+ ) > /dev/null 2>&1 &
423
+ fi
424
+
425
+ PIDS+=($!)
426
+ update_progress "$consultant" 10 "running"
427
+ done
428
+
429
+ # Log cache status
430
+ if [[ ${#CACHE_HITS[@]} -gt 0 ]]; then
431
+ log_info "Cache hits: ${CACHE_HITS[*]}"
432
+ fi
433
+
434
+ # --- Wait and Collect Results ---
435
+ log_info "Waiting for responses from ${#PIDS[@]} consultants..."
436
+
437
+ declare -a RESULTS=()
438
+ SUCCESS_COUNT=0
439
+
440
+ for i in "${!PIDS[@]}"; do
441
+ pid="${PIDS[$i]}"
442
+ name="${NAMES[$i]}"
443
+ output_file="${OUTPUT_FILES[$i]}"
444
+ name_lower=$(to_lower "$name")
445
+
446
+ # Handle cache hits (pid is "cache" marker)
447
+ if [[ "$pid" == "cache" ]]; then
448
+ if [[ -s "$output_file" ]]; then
449
+ log_success " $name: OK (cached)"
450
+ RESULTS+=("$name:OK")
451
+ SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
452
+ else
453
+ log_warn " $name: Cache miss"
454
+ RESULTS+=("$name:EMPTY")
455
+ fi
456
+ continue
457
+ fi
458
+
459
+ if wait "$pid" 2>/dev/null; then
460
+ if [[ -s "$output_file" ]]; then
461
+ log_success " $name: OK ($(wc -c < "$output_file" | tr -d ' ') bytes)"
462
+ RESULTS+=("$name:OK")
463
+ SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
464
+ update_progress "$name" 100 "success"
465
+
466
+ # Store in cache (v2.3)
467
+ if is_cache_enabled; then
468
+ response_content=$(cat "$output_file")
469
+ store_cache "$QUERY" "$QUESTION_CATEGORY" "$name_lower" "$response_content" "$CONTEXT_FILE" 2>/dev/null || true
470
+ fi
471
+ else
472
+ log_warn " $name: Empty response"
473
+ RESULTS+=("$name:EMPTY")
474
+ update_progress "$name" 100 "failed"
475
+ fi
476
+ else
477
+ log_error " $name: Failed"
478
+ RESULTS+=("$name:FAILED")
479
+ update_progress "$name" 100 "failed"
480
+ fi
481
+ done
482
+
483
+ echo "" >&2
484
+
485
+ # --- Budget Check: After Round 1 ---
486
+ CURRENT_COST=0
487
+ if [[ "$ENABLE_COST_TRACKING" == "true" && $SUCCESS_COUNT -gt 0 ]]; then
488
+ CURRENT_COST=$(calculate_session_cost "$OUTPUT_DIR")
489
+
490
+ # Check warning threshold
491
+ if check_warning_threshold "$CURRENT_COST"; then
492
+ log_warn "Cost warning: $(format_budget_status "$CURRENT_COST")"
493
+ fi
494
+
495
+ # Budget enforcement check
496
+ if is_budget_enabled; then
497
+ log_debug "Current cost after Round 1: $(format_cost "$CURRENT_COST")"
498
+ fi
499
+ fi
500
+
501
+ # --- Fallback Escalation: Re-query low-confidence with premium models ---
502
+ if is_escalation_enabled && [[ $SUCCESS_COUNT -gt 0 ]]; then
503
+ ESCALATED_COUNT=0
504
+ for consultant in "${SELECTED_CONSULTANTS[@]}"; do
505
+ consultant_lower=$(to_lower "$consultant")
506
+ response_file="$OUTPUT_DIR/${consultant_lower}.json"
507
+
508
+ if needs_escalation "$response_file"; then
509
+ premium_model=$(get_premium_model "$consultant_lower")
510
+ if [[ -n "$premium_model" ]]; then
511
+ original_confidence=$(jq -r '.confidence.score // 5' "$response_file" 2>/dev/null)
512
+ [[ "$original_confidence" =~ ^[0-9]+$ ]] || original_confidence=5
513
+ log_info "Escalating $consultant (confidence: $original_confidence) → premium model: $premium_model"
514
+
515
+ query_script="$SCRIPT_DIR/query_${consultant_lower}.sh"
516
+ if [[ -x "$query_script" ]]; then
517
+ escalation_file="$OUTPUT_DIR/${consultant_lower}_escalated.json"
518
+ model_var="$(echo "${consultant_lower}" | tr '[:lower:]' '[:upper:]')_MODEL"
519
+ if [[ ! "$model_var" =~ ^[A-Z0-9_]+_MODEL$ ]]; then
520
+ log_warn "Invalid model variable name: $model_var, skipping escalation for $consultant"
521
+ continue
522
+ fi
523
+ (
524
+ export "$model_var=$premium_model"
525
+ "$query_script" "" "$CONTEXT_FILE" "$escalation_file"
526
+ ) > /dev/null 2>&1
527
+
528
+ # Replace original if escalated response has higher confidence
529
+ if [[ -f "$escalation_file" && -s "$escalation_file" ]]; then
530
+ new_confidence=$(jq -r '.confidence.score // 0' "$escalation_file" 2>/dev/null)
531
+ [[ "$new_confidence" =~ ^[0-9]+$ ]] || new_confidence=0
532
+ if [[ "$new_confidence" -gt "$original_confidence" ]]; then
533
+ cp "$escalation_file" "$response_file"
534
+ log_success " $consultant escalated: confidence $original_confidence → $new_confidence"
535
+ ESCALATED_COUNT=$((ESCALATED_COUNT + 1))
536
+ else
537
+ log_debug " $consultant escalation did not improve confidence"
538
+ fi
539
+ fi
540
+ fi
541
+ fi
542
+ fi
543
+ done
544
+ if [[ $ESCALATED_COUNT -gt 0 ]]; then
545
+ log_info "Escalated $ESCALATED_COUNT consultant(s) to premium models"
546
+ fi
547
+ fi
548
+
549
+ # --- Panic Mode Detection (v2.2) ---
550
+ PANIC_TRIGGERED=false
551
+ if [[ "$ENABLE_PANIC_MODE" != "never" && $SUCCESS_COUNT -gt 1 ]]; then
552
+ if should_trigger_panic "$OUTPUT_DIR"; then
553
+ PANIC_TRIGGERED=true
554
+ PANIC_DIAGNOSIS=$(get_panic_diagnosis "$OUTPUT_DIR")
555
+
556
+ log_warn "PANIC MODE TRIGGERED - Uncertainty detected!"
557
+ log_warn " Diagnosis: $(echo "$PANIC_DIAGNOSIS" | jq -r '.triggers | join(", ")')"
558
+
559
+ # Save diagnosis
560
+ echo "$PANIC_DIAGNOSIS" > "$OUTPUT_DIR/panic_diagnosis.json"
561
+
562
+ # Actions: Enable debate if not already enabled, switch to risk_averse strategy
563
+ if [[ "$ENABLE_DEBATE" != "true" ]]; then
564
+ log_info " Action: Enabling multi-agent debate"
565
+ ENABLE_DEBATE=true
566
+ DEBATE_ROUNDS="${PANIC_EXTRA_DEBATE_ROUNDS:-1}"
567
+ DEBATE_ROUNDS=$((DEBATE_ROUNDS + 1)) # At least one extra round
568
+ else
569
+ # Add extra debate rounds
570
+ EXTRA_ROUNDS="${PANIC_EXTRA_DEBATE_ROUNDS:-1}"
571
+ DEBATE_ROUNDS=$((DEBATE_ROUNDS + EXTRA_ROUNDS))
572
+ log_info " Action: Adding $EXTRA_ROUNDS extra debate round(s) (total: $DEBATE_ROUNDS)"
573
+ fi
574
+
575
+ # Switch to risk_averse synthesis strategy
576
+ if [[ "$SYNTHESIS_STRATEGY" == "majority" ]]; then
577
+ log_info " Action: Switching to risk_averse synthesis strategy"
578
+ SYNTHESIS_STRATEGY="risk_averse"
579
+ export SYNTHESIS_STRATEGY
580
+ fi
581
+
582
+ echo "" >&2
583
+ fi
584
+ fi
585
+
586
+ # --- Round 2+: Multi-Agent Debate (optional) ---
587
+ if [[ "$ENABLE_DEBATE" == "true" && $DEBATE_ROUNDS -gt 1 && $SUCCESS_COUNT -gt 1 ]]; then
588
+ # Budget Check: Before Debate
589
+ if is_budget_enabled; then
590
+ DEBATE_ESTIMATE=$(estimate_phase_cost "debate" "$SUCCESS_COUNT" "$CONTEXT_SIZE")
591
+ DEBATE_ESTIMATE=$(echo "scale=6; $DEBATE_ESTIMATE * $DEBATE_ROUNDS" | bc)
592
+ if ! enforce_budget "$CURRENT_COST" "$DEBATE_ESTIMATE" "debate rounds"; then
593
+ log_warn "Skipping debate due to budget constraints"
594
+ ENABLE_DEBATE=false
595
+ fi
596
+ fi
597
+
598
+ # Proceed with debate if still enabled after budget check
599
+ if [[ "$ENABLE_DEBATE" == "true" ]]; then
600
+ log_info "Starting Multi-Agent Debate (${DEBATE_ROUNDS} total rounds)..."
601
+
602
+ for ((round=2; round<=DEBATE_ROUNDS; round++)); do
603
+ log_info " Round $round..."
604
+ # v2.3: Pass category for mandatory debate check (SECURITY/ARCHITECTURE always debate)
605
+ DEBATE_OUTPUT=$("$SCRIPT_DIR/debate_round.sh" "$OUTPUT_DIR" "$round" "$OUTPUT_DIR/round_$round" "$QUESTION_CATEGORY")
606
+
607
+ # Update responses with debate results
608
+ if [[ -d "$OUTPUT_DIR/round_$round" ]]; then
609
+ for f in "$OUTPUT_DIR/round_$round"/*.json; do
610
+ if [[ -f "$f" && "$f" != *"summary"* ]]; then
611
+ consultant=$(basename "$f" .json)
612
+ # Merge debate data into main response
613
+ if [[ -f "$OUTPUT_DIR/${consultant}.json" ]]; then
614
+ jq -s '.[0] * {debate: .[1].debate}' \
615
+ "$OUTPUT_DIR/${consultant}.json" "$f" \
616
+ > "$OUTPUT_DIR/${consultant}.json.tmp" && \
617
+ mv "$OUTPUT_DIR/${consultant}.json.tmp" "$OUTPUT_DIR/${consultant}.json"
618
+ fi
619
+ fi
620
+ done
621
+ fi
622
+ done
623
+ echo "" >&2
624
+ fi
625
+ fi
626
+
627
+ # --- Voting and Consensus ---
628
+ log_info "Calculating consensus and voting..."
629
+ VOTING_REPORT=$(generate_voting_report "$OUTPUT_DIR")
630
+ echo "$VOTING_REPORT" > "$OUTPUT_DIR/voting.json"
631
+
632
+ CONSENSUS_SCORE=$(echo "$VOTING_REPORT" | jq -r '.voting_report.consensus.score // 50')
633
+ CONSENSUS_LEVEL=$(echo "$VOTING_REPORT" | jq -r '.voting_report.consensus.level // "unknown"')
634
+ log_info " Consensus: ${CONSENSUS_SCORE}% ($CONSENSUS_LEVEL)"
635
+ echo "" >&2
636
+
637
+ # --- Quality Monitoring (v2.3) ---
638
+ # Log optimization metrics for quality tracking
639
+ if [[ "${LOG_LEVEL:-INFO}" == "DEBUG" ]]; then
640
+ log_debug "=== Token Optimization Metrics ==="
641
+ log_debug " Cache enabled: ${ENABLE_SEMANTIC_CACHE:-true}"
642
+ log_debug " Cache hits: ${#CACHE_HITS[@]}"
643
+ log_debug " Response limits enabled: ${ENABLE_RESPONSE_LIMITS:-false}"
644
+ log_debug " Cost-aware routing: ${ENABLE_COST_AWARE_ROUTING:-false}"
645
+ log_debug " Debate optimization: ${ENABLE_DEBATE_OPTIMIZATION:-false}"
646
+ fi
647
+
648
+ # Save optimization metrics to output directory
649
+ jq -n \
650
+ --argjson cache_enabled "${ENABLE_SEMANTIC_CACHE:-true}" \
651
+ --argjson cache_hits "${#CACHE_HITS[@]}" \
652
+ --argjson response_limits "${ENABLE_RESPONSE_LIMITS:-false}" \
653
+ --argjson cost_aware "${ENABLE_COST_AWARE_ROUTING:-false}" \
654
+ --argjson debate_opt "${ENABLE_DEBATE_OPTIMIZATION:-false}" \
655
+ --argjson compact "${ENABLE_COMPACT_REPORT:-true}" \
656
+ --argjson consensus "$CONSENSUS_SCORE" \
657
+ --arg consensus_level "$CONSENSUS_LEVEL" \
658
+ --argjson success_count "$SUCCESS_COUNT" \
659
+ --argjson total "${#PIDS[@]}" \
660
+ --arg category "$QUESTION_CATEGORY" \
661
+ '{
662
+ optimization_settings: {
663
+ cache_enabled: $cache_enabled,
664
+ cache_hits: $cache_hits,
665
+ response_limits_enabled: $response_limits,
666
+ cost_aware_routing: $cost_aware,
667
+ debate_optimization: $debate_opt,
668
+ compact_report: $compact
669
+ },
670
+ quality_metrics: {
671
+ consensus_score: $consensus,
672
+ consensus_level: $consensus_level,
673
+ successful_responses: $success_count,
674
+ total_consultants: $total,
675
+ category: $category
676
+ },
677
+ timestamp: (now | todate)
678
+ }' > "$OUTPUT_DIR/optimization_metrics.json"
679
+
680
+ # --- Auto-Synthesis (optional) ---
681
+ SYNTHESIS_FILE=""
682
+ if [[ "$ENABLE_SYNTHESIS" == "true" && $SUCCESS_COUNT -gt 0 ]]; then
683
+ # Budget Check: Before Synthesis
684
+ if is_budget_enabled; then
685
+ # Update current cost after debate
686
+ if [[ "$ENABLE_COST_TRACKING" == "true" ]]; then
687
+ CURRENT_COST=$(calculate_session_cost "$OUTPUT_DIR")
688
+ fi
689
+ SYNTHESIS_ESTIMATE=$(estimate_phase_cost "synthesis" 1 "$CONTEXT_SIZE")
690
+ if ! enforce_budget "$CURRENT_COST" "$SYNTHESIS_ESTIMATE" "synthesis"; then
691
+ log_warn "Skipping synthesis due to budget constraints"
692
+ ENABLE_SYNTHESIS=false
693
+ fi
694
+ fi
695
+
696
+ # Proceed with synthesis if still enabled after budget check
697
+ if [[ "$ENABLE_SYNTHESIS" == "true" ]]; then
698
+ log_info "Generating automatic synthesis..."
699
+ SYNTHESIS_FILE="$OUTPUT_DIR/synthesis.json"
700
+
701
+ if "$SCRIPT_DIR/synthesize.sh" "$OUTPUT_DIR" "$SYNTHESIS_FILE" "$QUERY" > /dev/null 2>&1; then
702
+ log_success "Synthesis generated: $SYNTHESIS_FILE"
703
+ else
704
+ log_warn "Synthesis failed, using fallback"
705
+ fi
706
+ echo "" >&2
707
+ fi
708
+ fi
709
+
710
+ # --- Anonymous Peer Review (v2.2, optional) ---
711
+ if [[ "${ENABLE_PEER_REVIEW:-false}" == "true" ]]; then
712
+ MIN_RESPONSES="${PEER_REVIEW_MIN_RESPONSES:-3}"
713
+ if [[ $SUCCESS_COUNT -ge $MIN_RESPONSES ]]; then
714
+ log_info "Running anonymous peer review..."
715
+ PEER_REVIEW_DIR="$OUTPUT_DIR/peer_review"
716
+ mkdir -p "$PEER_REVIEW_DIR"
717
+
718
+ if "$SCRIPT_DIR/peer_review.sh" "$OUTPUT_DIR" "$PEER_REVIEW_DIR" > /dev/null 2>&1; then
719
+ log_success "Peer review completed: $PEER_REVIEW_DIR"
720
+ else
721
+ log_warn "Peer review failed or unavailable"
722
+ fi
723
+ else
724
+ log_info "Skipping peer review: need ${MIN_RESPONSES}+ responses (got $SUCCESS_COUNT)"
725
+ fi
726
+ echo "" >&2
727
+ fi
728
+
729
+ # --- Generate Combined Report ---
730
+ log_info "Generating combined report..."
731
+
732
+ REPORT_FILE="$OUTPUT_DIR/report.md"
733
+
734
+ {
735
+ echo "# AI Consultation Report v2.0"
736
+ echo ""
737
+ echo "**Date**: $(date '+%Y-%m-%d %H:%M:%S')"
738
+ echo "**Directory**: $(pwd)"
739
+ echo "**Version**: AI Consultants v${AI_CONSULTANTS_VERSION}"
740
+ echo "**Category**: $QUESTION_CATEGORY"
741
+ echo "**Successes**: $SUCCESS_COUNT/${#PIDS[@]} consultants"
742
+ echo "**Consensus**: ${CONSENSUS_SCORE}% ($CONSENSUS_LEVEL)"
743
+ echo ""
744
+ echo "---"
745
+ echo ""
746
+
747
+ # Question
748
+ echo "## Question"
749
+ echo ""
750
+ echo "$QUERY"
751
+ echo ""
752
+
753
+ # Included files
754
+ if [[ ${#FILES[@]} -gt 0 ]]; then
755
+ echo "## Analyzed Files"
756
+ echo ""
757
+ for f in "${FILES[@]}"; do
758
+ echo "- \`$f\`"
759
+ done
760
+ echo ""
761
+ fi
762
+
763
+ # Quick Summary Table (v2.3)
764
+ echo "## Quick Summary"
765
+ echo ""
766
+ echo "| Consultant | Confidence | Approach |"
767
+ echo "|------------|------------|----------|"
768
+ for i in "${!NAMES[@]}"; do
769
+ name="${NAMES[$i]}"
770
+ output_file="${OUTPUT_FILES[$i]}"
771
+ result="${RESULTS[$i]}"
772
+ if [[ "$result" == *":OK" ]] && [[ -s "$output_file" ]]; then
773
+ conf=$(jq -r '.confidence.score // "?"' "$output_file" 2>/dev/null)
774
+ appr=$(jq -r '.response.approach // "N/A"' "$output_file" 2>/dev/null | head -c 30)
775
+ echo "| $name | $conf/10 | $appr |"
776
+ else
777
+ echo "| $name | - | (no response) |"
778
+ fi
779
+ done
780
+ echo ""
781
+
782
+ echo "---"
783
+ echo ""
784
+
785
+ # Synthesis (if available)
786
+ if [[ -n "$SYNTHESIS_FILE" && -f "$SYNTHESIS_FILE" ]]; then
787
+ echo "## Automatic Synthesis"
788
+ echo ""
789
+
790
+ recommendation=$(jq -r '.weighted_recommendation.summary // "N/A"' "$SYNTHESIS_FILE" 2>/dev/null)
791
+ approach=$(jq -r '.weighted_recommendation.approach // "N/A"' "$SYNTHESIS_FILE" 2>/dev/null)
792
+
793
+ echo "**Recommended Approach**: $approach"
794
+ echo ""
795
+ echo "$recommendation"
796
+ echo ""
797
+
798
+ # Comparison table (dynamic: reads actual consultant names from synthesis JSON)
799
+ echo "### Consultant Comparison"
800
+ echo ""
801
+
802
+ jq -r '
803
+ (.comparison_table[0] // {} | keys | map(select(. != "aspect"))) as $cs |
804
+ if ($cs | length) > 0 then
805
+ (["Aspect"] + $cs | map("| \(.) ") | join("")) + "|",
806
+ (["-"] + ($cs | map("-")) | map("|---") | join("")) + "|",
807
+ (.comparison_table[]? | . as $row |
808
+ ([.aspect] + [$cs[] as $c | ($row[$c] // "N/A")] |
809
+ map("| \(.) ") | join("")) + "|")
810
+ else
811
+ "| (comparison data unavailable) |"
812
+ end
813
+ ' "$SYNTHESIS_FILE" 2>/dev/null || echo "| (comparison data unavailable) |"
814
+
815
+ echo ""
816
+
817
+ # Risks
818
+ echo "### Risk Assessment"
819
+ echo ""
820
+ overall_risk=$(jq -r '.risk_assessment.overall_risk // "unknown"' "$SYNTHESIS_FILE" 2>/dev/null)
821
+ echo "**Overall Risk**: $overall_risk"
822
+ echo ""
823
+
824
+ jq -r '.risk_assessment.risks[]? | "- **\(.description)** (\(.severity)): \(.mitigation)"' \
825
+ "$SYNTHESIS_FILE" 2>/dev/null || echo "- No significant risks identified"
826
+
827
+ echo ""
828
+ echo "---"
829
+ echo ""
830
+ fi
831
+
832
+ echo "## Individual Responses"
833
+ echo ""
834
+
835
+ # Response from each consultant
836
+ for i in "${!NAMES[@]}"; do
837
+ name="${NAMES[$i]}"
838
+ output_file="${OUTPUT_FILES[$i]}"
839
+ result="${RESULTS[$i]}"
840
+
841
+ echo "### $name"
842
+ echo ""
843
+
844
+ if [[ "$result" == *":OK" ]] && [[ -s "$output_file" ]]; then
845
+ persona=$(jq -r '.persona // "N/A"' "$output_file" 2>/dev/null)
846
+ confidence=$(jq -r '.confidence.score // "N/A"' "$output_file" 2>/dev/null)
847
+ approach=$(jq -r '.response.approach // "N/A"' "$output_file" 2>/dev/null)
848
+ summary=$(jq -r '.response.summary // "No summary"' "$output_file" 2>/dev/null)
849
+
850
+ echo "**Persona**: $persona | **Confidence**: $confidence/10 | **Approach**: $approach"
851
+ echo ""
852
+ echo "$summary"
853
+ echo ""
854
+
855
+ # Include full JSON only if compact report is disabled (v2.3)
856
+ if [[ "${ENABLE_COMPACT_REPORT:-true}" != "true" ]]; then
857
+ max_lines="${REPORT_MAX_JSON_LINES:-50}"
858
+ echo "<details>"
859
+ echo "<summary>Full response</summary>"
860
+ echo ""
861
+ echo '```json'
862
+ jq '.' "$output_file" 2>/dev/null | head -"$max_lines"
863
+ if [[ $(wc -l < "$output_file") -gt $max_lines ]]; then
864
+ echo ""
865
+ echo "[... truncated, see ${output_file} for full response ...]"
866
+ fi
867
+ echo '```'
868
+ echo "</details>"
869
+ echo ""
870
+ fi
871
+ elif [[ "$result" == *":EMPTY" ]]; then
872
+ echo "*Empty response*"
873
+ else
874
+ echo "*Consultant did not respond*"
875
+ fi
876
+ echo ""
877
+ done
878
+
879
+ echo "---"
880
+ echo ""
881
+ echo "## Output Files"
882
+ echo ""
883
+ echo "- Context: \`$CONTEXT_FILE\`"
884
+ echo "- Voting: \`$OUTPUT_DIR/voting.json\`"
885
+ [[ -n "$SYNTHESIS_FILE" ]] && echo "- Synthesis: \`$SYNTHESIS_FILE\`"
886
+ for i in "${!NAMES[@]}"; do
887
+ echo "- ${NAMES[$i]}: \`${OUTPUT_FILES[$i]}\`"
888
+ done
889
+
890
+ } > "$REPORT_FILE"
891
+
892
+ log_success "Report generated: $REPORT_FILE"
893
+ echo "" >&2
894
+
895
+ # --- Session Management ---
896
+ SESSION_ID=$(save_session "$(generate_session_id)" "$QUERY" "$OUTPUT_DIR" "$QUESTION_CATEGORY")
897
+ log_info "Session saved: $SESSION_ID"
898
+
899
+ # --- Cost Tracking ---
900
+ if [[ "$ENABLE_COST_TRACKING" == "true" ]]; then
901
+ ACTUAL_COST=$(calculate_session_cost "$OUTPUT_DIR")
902
+ track_session_cost "$SESSION_ID" "$ACTUAL_COST"
903
+ log_info "Session cost: $(format_cost "$ACTUAL_COST")"
904
+ fi
905
+
906
+ # --- Final Summary ---
907
+ echo "" >&2
908
+ echo "════════════════════════════════════════════════════════════════" >&2
909
+ log_success "Consultation completed!"
910
+ echo "" >&2
911
+ log_info "Results:"
912
+ for result in "${RESULTS[@]}"; do
913
+ name="${result%%:*}"
914
+ status="${result##*:}"
915
+ case "$status" in
916
+ OK) log_success " - $name: Success" ;;
917
+ EMPTY) log_warn " - $name: Empty response" ;;
918
+ FAILED) log_error " - $name: Failed" ;;
919
+ esac
920
+ done
921
+ echo "" >&2
922
+ log_info "Consensus: ${CONSENSUS_SCORE}% ($CONSENSUS_LEVEL)"
923
+ echo "" >&2
924
+ log_info "To view the report:"
925
+ log_info " cat $REPORT_FILE"
926
+ echo "" >&2
927
+ log_info "For follow-up:"
928
+ log_info " ./followup.sh \"Your follow-up question\""
929
+ echo "" >&2
930
+ log_info "Output directory: $OUTPUT_DIR"
931
+ echo "════════════════════════════════════════════════════════════════" >&2
932
+
933
+ # Output path for calling scripts
934
+ echo "$OUTPUT_DIR"