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.
- package/.claude/commands/ai-consultants:consult.md +94 -0
- package/.claude/commands/ai-consultants:debate.md +103 -0
- package/.claude/commands/ai-consultants:help.md +30 -0
- package/CLAUDE.md +839 -0
- package/LICENSE +21 -0
- package/README.md +758 -0
- package/SKILL.md +156 -0
- package/bin/ai-consultants +96 -0
- package/docs/COST_RATES.md +172 -0
- package/docs/JSON_SCHEMA.md +305 -0
- package/docs/SETUP.md +879 -0
- package/docs/SMART_ROUTING.md +219 -0
- package/docs/cost_rates.json +143 -0
- package/package.json +47 -0
- package/references/configuration.md +141 -0
- package/references/details.md +60 -0
- package/scripts/build_context.sh +562 -0
- package/scripts/classify_question.sh +148 -0
- package/scripts/config.sh +753 -0
- package/scripts/configure.sh +1093 -0
- package/scripts/consult_all.sh +934 -0
- package/scripts/debate_round.sh +421 -0
- package/scripts/doctor.sh +642 -0
- package/scripts/followup.sh +197 -0
- package/scripts/install.sh +319 -0
- package/scripts/lib/api.sh +640 -0
- package/scripts/lib/api_query.sh +228 -0
- package/scripts/lib/cache.sh +338 -0
- package/scripts/lib/chunking.sh +880 -0
- package/scripts/lib/code_optimizer.sh +671 -0
- package/scripts/lib/common.sh +1088 -0
- package/scripts/lib/costs.sh +674 -0
- package/scripts/lib/personas.sh +372 -0
- package/scripts/lib/progress.sh +251 -0
- package/scripts/lib/reflection.sh +447 -0
- package/scripts/lib/routing.sh +566 -0
- package/scripts/lib/schema.json +165 -0
- package/scripts/lib/session.sh +327 -0
- package/scripts/lib/symbol_map.sh +445 -0
- package/scripts/lib/voting.sh +495 -0
- package/scripts/peer_review.sh +460 -0
- package/scripts/preflight_check.sh +452 -0
- package/scripts/query_aider.sh +83 -0
- package/scripts/query_amp.sh +67 -0
- package/scripts/query_claude.sh +99 -0
- package/scripts/query_codex.sh +92 -0
- package/scripts/query_cursor.sh +72 -0
- package/scripts/query_deepseek.sh +17 -0
- package/scripts/query_gemini.sh +85 -0
- package/scripts/query_glm.sh +17 -0
- package/scripts/query_grok.sh +17 -0
- package/scripts/query_kilo.sh +140 -0
- package/scripts/query_kimi.sh +67 -0
- package/scripts/query_minimax.sh +17 -0
- package/scripts/query_mistral.sh +97 -0
- package/scripts/query_ollama.sh +328 -0
- package/scripts/query_qwen3.sh +93 -0
- package/scripts/setup_wizard.sh +367 -0
- package/scripts/skillport-install.sh +196 -0
- package/scripts/synthesize.sh +332 -0
- package/templates/consultation_report.md +323 -0
- package/templates/synthesis_prompt.md +120 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# peer_review.sh - Anonymous Peer Review for AI Consultants
|
|
3
|
+
#
|
|
4
|
+
# Implements blind peer review where consultants evaluate each other's
|
|
5
|
+
# responses without knowing the source, reducing bias and identifying
|
|
6
|
+
# the strongest arguments.
|
|
7
|
+
#
|
|
8
|
+
# Process:
|
|
9
|
+
# 1. Anonymize all responses (remove consultant names/identifiers)
|
|
10
|
+
# 2. Each consultant ranks and critiques top 2-3 responses
|
|
11
|
+
# 3. Aggregate peer scores to identify strongest arguments
|
|
12
|
+
# 4. De-anonymize in final report with peer scores
|
|
13
|
+
#
|
|
14
|
+
# Usage: ./peer_review.sh <responses_dir> <output_dir>
|
|
15
|
+
#
|
|
16
|
+
# Input: Directory containing consultant JSON responses
|
|
17
|
+
# Output: Directory with peer review results and aggregated scores
|
|
18
|
+
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
22
|
+
source "$SCRIPT_DIR/lib/common.sh"
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# CONFIGURATION
|
|
26
|
+
# =============================================================================
|
|
27
|
+
|
|
28
|
+
# Number of responses each consultant reviews (2-3 recommended)
|
|
29
|
+
REVIEWS_PER_CONSULTANT="${REVIEWS_PER_CONSULTANT:-3}"
|
|
30
|
+
|
|
31
|
+
# Minimum consultants needed for peer review
|
|
32
|
+
MIN_CONSULTANTS_FOR_REVIEW=3
|
|
33
|
+
|
|
34
|
+
# =============================================================================
|
|
35
|
+
# ANONYMIZATION
|
|
36
|
+
# =============================================================================
|
|
37
|
+
|
|
38
|
+
# Create anonymous ID from consultant name
|
|
39
|
+
# Uses a simple hash-like approach for deterministic but hidden IDs
|
|
40
|
+
generate_anonymous_id() {
|
|
41
|
+
local consultant="$1"
|
|
42
|
+
local index="$2"
|
|
43
|
+
echo "Response_$(printf '%c' $((65 + index)))" # A, B, C, D, etc.
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Anonymize a response by removing identifying information
|
|
47
|
+
# Usage: anonymize_response <response_file> <anonymous_id>
|
|
48
|
+
anonymize_response() {
|
|
49
|
+
local response_file="$1"
|
|
50
|
+
local anon_id="$2"
|
|
51
|
+
|
|
52
|
+
# Remove consultant name and persona, keep the substance
|
|
53
|
+
jq --arg anon_id "$anon_id" '
|
|
54
|
+
del(.consultant, .persona, .model) |
|
|
55
|
+
. + {anonymous_id: $anon_id}
|
|
56
|
+
' "$response_file" 2>/dev/null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Create mapping file for de-anonymization later
|
|
60
|
+
# Usage: create_mapping <responses_dir> <output_file>
|
|
61
|
+
create_mapping() {
|
|
62
|
+
local responses_dir="$1"
|
|
63
|
+
local output_file="$2"
|
|
64
|
+
|
|
65
|
+
local mapping="{}"
|
|
66
|
+
local index=0
|
|
67
|
+
|
|
68
|
+
for f in "$responses_dir"/*.json; do
|
|
69
|
+
if [[ -f "$f" && -s "$f" ]]; then
|
|
70
|
+
local consultant
|
|
71
|
+
consultant=$(jq -r '.consultant // "unknown"' "$f" 2>/dev/null)
|
|
72
|
+
local anon_id
|
|
73
|
+
anon_id=$(generate_anonymous_id "$consultant" "$index")
|
|
74
|
+
|
|
75
|
+
mapping=$(echo "$mapping" | jq \
|
|
76
|
+
--arg anon "$anon_id" \
|
|
77
|
+
--arg real "$consultant" \
|
|
78
|
+
--arg file "$(basename "$f")" \
|
|
79
|
+
'. + {($anon): {consultant: $real, file: $file}}')
|
|
80
|
+
|
|
81
|
+
((index++))
|
|
82
|
+
fi
|
|
83
|
+
done
|
|
84
|
+
|
|
85
|
+
echo "$mapping" > "$output_file"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# =============================================================================
|
|
89
|
+
# PEER REVIEW PROMPTS
|
|
90
|
+
# =============================================================================
|
|
91
|
+
|
|
92
|
+
# Generate the peer review prompt
|
|
93
|
+
generate_review_prompt() {
|
|
94
|
+
local anonymous_responses="$1"
|
|
95
|
+
|
|
96
|
+
cat << EOF
|
|
97
|
+
You are an expert peer reviewer evaluating anonymous responses to a coding question.
|
|
98
|
+
Review each response objectively without knowing who wrote it.
|
|
99
|
+
|
|
100
|
+
## Anonymous Responses to Review
|
|
101
|
+
|
|
102
|
+
$anonymous_responses
|
|
103
|
+
|
|
104
|
+
## Your Task
|
|
105
|
+
|
|
106
|
+
For each response, provide:
|
|
107
|
+
1. **Quality Score** (1-10): Overall quality of the response
|
|
108
|
+
2. **Strengths**: What the response does well (2-3 points)
|
|
109
|
+
3. **Weaknesses**: What could be improved (2-3 points)
|
|
110
|
+
4. **Ranking**: Rank all responses from best to worst
|
|
111
|
+
|
|
112
|
+
Respond ONLY with valid JSON:
|
|
113
|
+
{
|
|
114
|
+
"reviews": [
|
|
115
|
+
{
|
|
116
|
+
"response_id": "Response_A",
|
|
117
|
+
"quality_score": <1-10>,
|
|
118
|
+
"strengths": ["<strength1>", "<strength2>"],
|
|
119
|
+
"weaknesses": ["<weakness1>", "<weakness2>"],
|
|
120
|
+
"key_insight": "<most valuable insight from this response>"
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
"ranking": ["Response_A", "Response_B", ...],
|
|
124
|
+
"best_overall": "<response_id with best overall answer>",
|
|
125
|
+
"reasoning": "<why you ranked them this way>"
|
|
126
|
+
}
|
|
127
|
+
EOF
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# =============================================================================
|
|
131
|
+
# PEER REVIEW EXECUTION
|
|
132
|
+
# =============================================================================
|
|
133
|
+
|
|
134
|
+
# Run peer review for a single consultant
|
|
135
|
+
# Usage: run_consultant_review <consultant> <anonymous_file> <output_file>
|
|
136
|
+
run_consultant_review() {
|
|
137
|
+
local consultant="$1"
|
|
138
|
+
local anonymous_file="$2"
|
|
139
|
+
local output_file="$3"
|
|
140
|
+
|
|
141
|
+
local prompt
|
|
142
|
+
prompt=$(generate_review_prompt "$(cat "$anonymous_file")")
|
|
143
|
+
|
|
144
|
+
local timeout_var="${consultant^^}_TIMEOUT_SECONDS"
|
|
145
|
+
local timeout="${!timeout_var:-180}"
|
|
146
|
+
|
|
147
|
+
log_debug "Running peer review by $consultant..."
|
|
148
|
+
|
|
149
|
+
case "$consultant" in
|
|
150
|
+
Gemini) echo "$prompt" | run_with_timeout "$timeout" "$GEMINI_CMD" -p - -m "$GEMINI_MODEL" ;;
|
|
151
|
+
Codex) run_with_timeout "$timeout" "$CODEX_CMD" exec --skip-git-repo-check "$prompt" ;;
|
|
152
|
+
Mistral) run_with_timeout "$timeout" "$MISTRAL_CMD" --prompt "$prompt" --auto-approve ;;
|
|
153
|
+
Kilo) run_with_timeout "$timeout" "$KILO_CMD" --auto --json "$prompt" ;;
|
|
154
|
+
Cursor) run_with_timeout "$timeout" "$CURSOR_CMD" "$prompt" ;;
|
|
155
|
+
*)
|
|
156
|
+
echo '{"error": "Unknown consultant for peer review"}' > "$output_file"
|
|
157
|
+
return 1
|
|
158
|
+
;;
|
|
159
|
+
esac > "$output_file" 2>/dev/null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Extract JSON from response (may have text wrapper)
|
|
163
|
+
extract_json_from_response() {
|
|
164
|
+
local input="$1"
|
|
165
|
+
|
|
166
|
+
# Try to extract JSON
|
|
167
|
+
if echo "$input" | jq -e '.' > /dev/null 2>&1; then
|
|
168
|
+
echo "$input"
|
|
169
|
+
return 0
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# Try to extract from markdown code block
|
|
173
|
+
local extracted
|
|
174
|
+
extracted=$(echo "$input" | sed -n '/```json/,/```/p' | sed '1d;$d')
|
|
175
|
+
if [[ -n "$extracted" ]] && echo "$extracted" | jq -e '.' > /dev/null 2>&1; then
|
|
176
|
+
echo "$extracted"
|
|
177
|
+
return 0
|
|
178
|
+
fi
|
|
179
|
+
|
|
180
|
+
# Try to find first { to last }
|
|
181
|
+
extracted=$(echo "$input" | sed -n '/{/,/}/p')
|
|
182
|
+
if [[ -n "$extracted" ]] && echo "$extracted" | jq -e '.' > /dev/null 2>&1; then
|
|
183
|
+
echo "$extracted"
|
|
184
|
+
return 0
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
echo '{"error": "Could not extract JSON from response"}'
|
|
188
|
+
return 1
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# =============================================================================
|
|
192
|
+
# AGGREGATION
|
|
193
|
+
# =============================================================================
|
|
194
|
+
|
|
195
|
+
# Aggregate peer review scores
|
|
196
|
+
# Usage: aggregate_peer_scores <reviews_dir> <mapping_file> <output_file>
|
|
197
|
+
aggregate_peer_scores() {
|
|
198
|
+
local reviews_dir="$1"
|
|
199
|
+
local mapping_file="$2"
|
|
200
|
+
local output_file="$3"
|
|
201
|
+
|
|
202
|
+
local mapping
|
|
203
|
+
mapping=$(cat "$mapping_file")
|
|
204
|
+
|
|
205
|
+
# Initialize score tracking
|
|
206
|
+
local aggregated='{}'
|
|
207
|
+
|
|
208
|
+
# Process each review file
|
|
209
|
+
for review_file in "$reviews_dir"/review_*.json; do
|
|
210
|
+
if [[ ! -f "$review_file" ]]; then
|
|
211
|
+
continue
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
local reviewer
|
|
215
|
+
reviewer=$(basename "$review_file" .json | sed 's/review_//')
|
|
216
|
+
|
|
217
|
+
local review_content
|
|
218
|
+
review_content=$(cat "$review_file")
|
|
219
|
+
|
|
220
|
+
# Extract reviews array
|
|
221
|
+
local reviews
|
|
222
|
+
reviews=$(echo "$review_content" | jq -r '.reviews // []' 2>/dev/null)
|
|
223
|
+
|
|
224
|
+
# Process each individual review (using process substitution to avoid subshell)
|
|
225
|
+
while read -r review; do
|
|
226
|
+
[[ -z "$review" ]] && continue
|
|
227
|
+
local response_id score
|
|
228
|
+
response_id=$(echo "$review" | jq -r '.response_id // ""')
|
|
229
|
+
score=$(echo "$review" | jq -r '.quality_score // 5')
|
|
230
|
+
|
|
231
|
+
if [[ -n "$response_id" ]]; then
|
|
232
|
+
# Update aggregated scores
|
|
233
|
+
aggregated=$(echo "$aggregated" | jq \
|
|
234
|
+
--arg id "$response_id" \
|
|
235
|
+
--argjson score "$score" \
|
|
236
|
+
--arg reviewer "$reviewer" \
|
|
237
|
+
'
|
|
238
|
+
if .[$id] then
|
|
239
|
+
.[$id].scores += [$score] |
|
|
240
|
+
.[$id].reviewers += [$reviewer] |
|
|
241
|
+
.[$id].total += $score |
|
|
242
|
+
.[$id].count += 1
|
|
243
|
+
else
|
|
244
|
+
.[$id] = {scores: [$score], reviewers: [$reviewer], total: $score, count: 1}
|
|
245
|
+
end
|
|
246
|
+
')
|
|
247
|
+
fi
|
|
248
|
+
done < <(echo "$reviews" | jq -c '.[]' 2>/dev/null)
|
|
249
|
+
done
|
|
250
|
+
|
|
251
|
+
# Calculate averages and de-anonymize
|
|
252
|
+
local final_results='[]'
|
|
253
|
+
|
|
254
|
+
for anon_id in $(echo "$aggregated" | jq -r 'keys[]' 2>/dev/null); do
|
|
255
|
+
local data
|
|
256
|
+
data=$(echo "$aggregated" | jq --arg id "$anon_id" '.[$id]')
|
|
257
|
+
|
|
258
|
+
local total count avg_score
|
|
259
|
+
total=$(echo "$data" | jq -r '.total // 0')
|
|
260
|
+
count=$(echo "$data" | jq -r '.count // 1')
|
|
261
|
+
avg_score=$((total / count))
|
|
262
|
+
|
|
263
|
+
# De-anonymize
|
|
264
|
+
local real_consultant
|
|
265
|
+
real_consultant=$(echo "$mapping" | jq -r --arg id "$anon_id" '.[$id].consultant // "unknown"')
|
|
266
|
+
|
|
267
|
+
final_results=$(echo "$final_results" | jq \
|
|
268
|
+
--arg anon "$anon_id" \
|
|
269
|
+
--arg real "$real_consultant" \
|
|
270
|
+
--argjson avg "$avg_score" \
|
|
271
|
+
--argjson count "$count" \
|
|
272
|
+
--argjson data "$data" \
|
|
273
|
+
'. + [{
|
|
274
|
+
anonymous_id: $anon,
|
|
275
|
+
consultant: $real,
|
|
276
|
+
average_peer_score: $avg,
|
|
277
|
+
review_count: $count,
|
|
278
|
+
individual_scores: $data.scores
|
|
279
|
+
}]')
|
|
280
|
+
done
|
|
281
|
+
|
|
282
|
+
# Sort by average score descending
|
|
283
|
+
echo "$final_results" | jq 'sort_by(-.average_peer_score)' > "$output_file"
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# =============================================================================
|
|
287
|
+
# MAIN
|
|
288
|
+
# =============================================================================
|
|
289
|
+
|
|
290
|
+
main() {
|
|
291
|
+
local responses_dir="${1:-}"
|
|
292
|
+
local output_dir="${2:-}"
|
|
293
|
+
|
|
294
|
+
# Validate inputs
|
|
295
|
+
if [[ -z "$responses_dir" || ! -d "$responses_dir" ]]; then
|
|
296
|
+
log_error "Usage: $0 <responses_dir> <output_dir>"
|
|
297
|
+
exit 1
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
# Count available responses
|
|
301
|
+
local response_count=0
|
|
302
|
+
for f in "$responses_dir"/*.json; do
|
|
303
|
+
if [[ -f "$f" && -s "$f" && "$(basename "$f")" != "voting.json" && "$(basename "$f")" != "synthesis.json" ]]; then
|
|
304
|
+
((response_count++))
|
|
305
|
+
fi
|
|
306
|
+
done
|
|
307
|
+
|
|
308
|
+
if [[ $response_count -lt $MIN_CONSULTANTS_FOR_REVIEW ]]; then
|
|
309
|
+
log_warn "Peer review requires at least $MIN_CONSULTANTS_FOR_REVIEW responses (found $response_count)"
|
|
310
|
+
log_info "Skipping peer review"
|
|
311
|
+
exit 0
|
|
312
|
+
fi
|
|
313
|
+
|
|
314
|
+
# Setup output directory
|
|
315
|
+
output_dir="${output_dir:-$responses_dir/peer_review}"
|
|
316
|
+
mkdir -p "$output_dir"
|
|
317
|
+
|
|
318
|
+
log_info "Starting anonymous peer review..."
|
|
319
|
+
log_info "Responses to review: $response_count"
|
|
320
|
+
|
|
321
|
+
# Step 1: Create anonymized versions and mapping
|
|
322
|
+
log_info "Step 1: Anonymizing responses..."
|
|
323
|
+
|
|
324
|
+
local mapping_file="$output_dir/mapping.json"
|
|
325
|
+
local anonymous_dir="$output_dir/anonymous"
|
|
326
|
+
mkdir -p "$anonymous_dir"
|
|
327
|
+
|
|
328
|
+
create_mapping "$responses_dir" "$mapping_file"
|
|
329
|
+
|
|
330
|
+
local index=0
|
|
331
|
+
local anonymous_content=""
|
|
332
|
+
|
|
333
|
+
for f in "$responses_dir"/*.json; do
|
|
334
|
+
if [[ -f "$f" && -s "$f" && "$(basename "$f")" != "voting.json" && "$(basename "$f")" != "synthesis.json" ]]; then
|
|
335
|
+
local consultant
|
|
336
|
+
consultant=$(jq -r '.consultant // "unknown"' "$f" 2>/dev/null)
|
|
337
|
+
local anon_id
|
|
338
|
+
anon_id=$(generate_anonymous_id "$consultant" "$index")
|
|
339
|
+
|
|
340
|
+
# Create anonymized file
|
|
341
|
+
anonymize_response "$f" "$anon_id" > "$anonymous_dir/${anon_id}.json"
|
|
342
|
+
|
|
343
|
+
# Build combined anonymous content for review
|
|
344
|
+
local summary approach
|
|
345
|
+
summary=$(jq -r '.response.summary // "No summary"' "$f" 2>/dev/null)
|
|
346
|
+
approach=$(jq -r '.response.approach // "Unknown"' "$f" 2>/dev/null)
|
|
347
|
+
|
|
348
|
+
anonymous_content+="
|
|
349
|
+
### $anon_id
|
|
350
|
+
**Approach**: $approach
|
|
351
|
+
**Summary**: $summary
|
|
352
|
+
---
|
|
353
|
+
"
|
|
354
|
+
((index++))
|
|
355
|
+
fi
|
|
356
|
+
done
|
|
357
|
+
|
|
358
|
+
# Save combined anonymous responses
|
|
359
|
+
echo "$anonymous_content" > "$output_dir/anonymous_responses.md"
|
|
360
|
+
log_success "Created $index anonymous responses"
|
|
361
|
+
|
|
362
|
+
# Step 2: Run peer reviews (excluding self-review)
|
|
363
|
+
log_info "Step 2: Running peer reviews..."
|
|
364
|
+
|
|
365
|
+
local reviews_dir="$output_dir/reviews"
|
|
366
|
+
mkdir -p "$reviews_dir"
|
|
367
|
+
|
|
368
|
+
# Determine which consultants can do reviews
|
|
369
|
+
local reviewers=()
|
|
370
|
+
[[ "$ENABLE_GEMINI" == "true" ]] && command -v "$GEMINI_CMD" &>/dev/null && reviewers+=("Gemini")
|
|
371
|
+
[[ "$ENABLE_CODEX" == "true" ]] && command -v "$CODEX_CMD" &>/dev/null && reviewers+=("Codex")
|
|
372
|
+
[[ "$ENABLE_MISTRAL" == "true" ]] && command -v "$MISTRAL_CMD" &>/dev/null && reviewers+=("Mistral")
|
|
373
|
+
[[ "$ENABLE_KILO" == "true" ]] && command -v "$KILO_CMD" &>/dev/null && reviewers+=("Kilo")
|
|
374
|
+
[[ "$ENABLE_CURSOR" == "true" ]] && command -v "$CURSOR_CMD" &>/dev/null && reviewers+=("Cursor")
|
|
375
|
+
|
|
376
|
+
if [[ ${#reviewers[@]} -lt 2 ]]; then
|
|
377
|
+
log_warn "Need at least 2 reviewers for peer review"
|
|
378
|
+
exit 0
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
# Run reviews in parallel
|
|
382
|
+
local pids=()
|
|
383
|
+
for reviewer in "${reviewers[@]}"; do
|
|
384
|
+
local review_output="$reviews_dir/review_${reviewer}.json"
|
|
385
|
+
run_consultant_review "$reviewer" "$output_dir/anonymous_responses.md" "$review_output" &
|
|
386
|
+
pids+=($!)
|
|
387
|
+
log_debug "Started review by $reviewer (PID: ${pids[-1]})"
|
|
388
|
+
done
|
|
389
|
+
|
|
390
|
+
# Wait for all reviews
|
|
391
|
+
local success_count=0
|
|
392
|
+
for i in "${!pids[@]}"; do
|
|
393
|
+
if wait "${pids[$i]}" 2>/dev/null; then
|
|
394
|
+
local reviewer="${reviewers[$i]}"
|
|
395
|
+
local review_file="$reviews_dir/review_${reviewer}.json"
|
|
396
|
+
|
|
397
|
+
if [[ -s "$review_file" ]]; then
|
|
398
|
+
# Try to extract and validate JSON
|
|
399
|
+
local extracted
|
|
400
|
+
extracted=$(extract_json_from_response "$(cat "$review_file")")
|
|
401
|
+
echo "$extracted" > "$review_file"
|
|
402
|
+
|
|
403
|
+
if echo "$extracted" | jq -e '.reviews' > /dev/null 2>&1; then
|
|
404
|
+
log_success " $reviewer: review completed"
|
|
405
|
+
((success_count++))
|
|
406
|
+
else
|
|
407
|
+
log_warn " $reviewer: invalid review format"
|
|
408
|
+
fi
|
|
409
|
+
fi
|
|
410
|
+
else
|
|
411
|
+
log_warn " ${reviewers[$i]}: review failed"
|
|
412
|
+
fi
|
|
413
|
+
done
|
|
414
|
+
|
|
415
|
+
if [[ $success_count -lt 2 ]]; then
|
|
416
|
+
log_error "Not enough successful reviews for aggregation"
|
|
417
|
+
exit 1
|
|
418
|
+
fi
|
|
419
|
+
|
|
420
|
+
# Step 3: Aggregate scores
|
|
421
|
+
log_info "Step 3: Aggregating peer scores..."
|
|
422
|
+
|
|
423
|
+
local aggregated_file="$output_dir/aggregated_scores.json"
|
|
424
|
+
aggregate_peer_scores "$reviews_dir" "$mapping_file" "$aggregated_file"
|
|
425
|
+
|
|
426
|
+
log_success "Peer review complete"
|
|
427
|
+
|
|
428
|
+
# Step 4: Generate summary report
|
|
429
|
+
log_info "Step 4: Generating summary..."
|
|
430
|
+
|
|
431
|
+
local summary_file="$output_dir/peer_review_summary.json"
|
|
432
|
+
jq -n \
|
|
433
|
+
--argjson scores "$(cat "$aggregated_file")" \
|
|
434
|
+
--argjson mapping "$(cat "$mapping_file")" \
|
|
435
|
+
--argjson review_count "$success_count" \
|
|
436
|
+
--argjson response_count "$response_count" \
|
|
437
|
+
'{
|
|
438
|
+
peer_review: {
|
|
439
|
+
total_responses: $response_count,
|
|
440
|
+
total_reviews: $review_count,
|
|
441
|
+
results: $scores,
|
|
442
|
+
best_rated: ($scores | first),
|
|
443
|
+
mapping: $mapping
|
|
444
|
+
}
|
|
445
|
+
}' > "$summary_file"
|
|
446
|
+
|
|
447
|
+
# Print results
|
|
448
|
+
if [[ -s "$aggregated_file" ]]; then
|
|
449
|
+
echo ""
|
|
450
|
+
log_info "Peer Review Results (ranked by peer score):"
|
|
451
|
+
echo ""
|
|
452
|
+
jq -r '.[] | " \(.consultant): \(.average_peer_score)/10 (reviewed by \(.review_count) peers)"' "$aggregated_file"
|
|
453
|
+
echo ""
|
|
454
|
+
fi
|
|
455
|
+
|
|
456
|
+
log_info "Output directory: $output_dir"
|
|
457
|
+
echo "$output_dir"
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
main "$@"
|