
Parallel Test Bench
- 1 installs
- Updated March 16, 2026
- 89jobrien/pjlib
parallel-test-bench is a Claude Code skill that runs parallel agent experiments with automatic timing/token metric collection, programmatic grading, and benchmark aggregation for A/B testing approaches.
About
parallel-test-bench is a Claude Code skill for running parallel agent experiments with automatic metric collection and benchmark aggregation. It defines a five-phase workflow (setup, spawn, collect, grade, aggregate) that spawns multiple agents in one message to A/B test prompts, tools, or configurations. A developer uses it to compare a baseline against a treatment, capture timing and token usage, and grade outputs against programmatic assertions. Results are aggregated into a benchmark report for side-by-side comparison.
- Runs parallel agent experiments to A/B test approaches and configurations
- Collects timing and token metrics per run and grades outputs against assertions
- Aggregates results into benchmark reports with a bundled Python script
Parallel Test Bench by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,750 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
parallel-test-bench capabilities & compatibility
- Capabilities
- ab testing · benchmarking · agent evaluation · metric collection
- Use cases
- testing
- Pricing
- Free
What parallel-test-bench says it does
Infrastructure for running parallel agent tests with automatic metric collection, programmatic grading, and benchmark aggregation.
Spawn ALL agents in a SINGLE message using multiple Agent tool calls. This ensures they run truly in parallel.
npx skills add https://github.com/89jobrien/pjlib --skill parallel-test-benchAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| Last updated | March 16, 2026 |
| Repository | 89jobrien/pjlib ↗ |
What it does
Run parallel agent A/B tests, collect timing and token metrics, grade outputs, and aggregate benchmark comparisons.
Who is it for?
A/B testing prompts, tools, or agent configurations with measured timing, token, and pass/fail metrics
Skip if: Standard unit or integration testing of application code
When should I use this skill?
You want to test in parallel, compare approaches, benchmark, or A/B test agent configurations
What you get
A benchmark report comparing baseline vs treatment on timing, tokens, and graded assertions.
- timing.json per run
- grading.json per run
- benchmark.json and benchmark.md report
By the numbers
- 5-phase workflow (setup, spawn, collect, grade, aggregate)
- recommends 2-3 test cases minimum
Files
Parallel Test Bench
You are an expert at designing and running parallel agent experiments with automatic metric collection and benchmark aggregation.
When to Use This Skill
Use this skill whenever you need to:
- Compare multiple approaches or implementations side-by-side
- A/B test different prompts, tools, or configurations
- Benchmark performance improvements (before/after comparisons)
- Run experiments with different agent settings
- Collect timing and token usage metrics systematically
- Generate comparison reports with statistical analysis
Core Workflow
The parallel testing workflow has 5 phases:
1. Setup - Define test configurations and create workspace structure 2. Spawn - Launch all agents in parallel (one message, multiple Agent tool calls) 3. Collect - Capture timing/token metrics from task notifications 4. Grade - Evaluate outputs against assertions programmatically 5. Aggregate - Generate benchmark reports with comparisons
Phase 1: Setup
Define Test Configurations
Before spawning agents, clarify with the user:
Test name: What are you testing? (e.g., "emoji-removal", "refactoring-approach")
Configurations: What variations to test?
- Baseline: The control group (e.g., no skill, old version, simple approach)
- Treatment: The experimental group (e.g., with skill, new version, optimized approach)
- Can have 2+ configurations (e.g., baseline vs approach-A vs approach-B)
Test cases: What specific tasks to run?
- Each test case needs: prompt, expected output description, optional input files
- Aim for 2-3 test cases minimum
- Each case tests a different aspect or edge case
Success criteria: How to measure success?
- Quantitative: assertions that can be checked programmatically
- Qualitative: aspects requiring human judgment
Create Workspace Structure
<test-name>-workspace/
└── iteration-1/
├── test-case-1/
│ ├── baseline/
│ │ ├── outputs/
│ │ ├── timing.json
│ │ └── grading.json
│ └── treatment/
│ ├── outputs/
│ ├── timing.json
│ └── grading.json
├── test-case-2/
│ └── ...
├── benchmark.json
└── benchmark.mdDon't create all directories upfront - create them as you go when saving outputs.
Phase 2: Spawn Agents in Parallel
CRITICAL: Spawn ALL agents in a SINGLE message using multiple Agent tool calls. This ensures they run truly in parallel.
For each test case, spawn one agent per configuration:
Test Case: <descriptive-name>
Baseline agent:
- Configuration: <baseline description>
- Task: <test prompt>
- Save outputs to: workspace/iteration-1/<test-case>/baseline/outputs/
Treatment agent:
- Configuration: <treatment description>
- Task: <same test prompt>
- Save outputs to: workspace/iteration-1/<test-case>/treatment/outputs/Example agent spawning (all in one message):
I'm spawning 4 agents in parallel (2 test cases × 2 configurations):Then use 4 Agent tool calls in the same response.
Agent Prompt Template
Each agent should receive clear instructions:
Execute this task:
- Configuration: <config description, e.g., "without emoji-remover skill" or "with skill at path/to/skill">
- Task: <user's test prompt>
- Input files: <paths to input files, or "none">
- Save all outputs to: <workspace-path>/outputs/
- Outputs to save: <what matters - e.g., "the cleaned markdown file", "the refactored code">Phase 3: Collect Metrics
When each agent completes, you receive a task notification with:
total_tokens: Token countduration_ms: Execution time in milliseconds
Save this data IMMEDIATELY - it's only available in the notification:
{
"total_tokens": 18534,
"duration_ms": 16408,
"total_duration_seconds": 16.4
}Save to <test-case>/<config>/timing.json.
Tracking Completions
As agents complete, keep a mental checklist:
- [ ] test-case-1/baseline - tokens: X, time: Ys
- [ ] test-case-1/treatment - tokens: X, time: Ys
- [ ] test-case-2/baseline - tokens: X, time: Ys
- [ ] test-case-2/treatment - tokens: X, time: Ys
Once all complete, proceed to grading.
Phase 4: Grade Outputs
For each configuration output, evaluate against assertions.
Programmatic Grading
Use the bundled grading script for objective checks:
python scripts/grade_outputs.py \
--test-case <path-to-test-case-dir> \
--config baseline \
--assertions <assertions-json>Or write custom grading logic inline:
# Example: Check no emojis remaining
import re
EMOJI_PATTERN = re.compile(r'[\U0001F300-\U0001F9FF]')
with open(output_file) as f:
content = f.read()
has_emojis = bool(EMOJI_PATTERN.search(content))
grading = {
"expectations": [
{
"text": "No emojis in output",
"passed": not has_emojis,
"evidence": f"Found {len(EMOJI_PATTERN.findall(content))} emojis" if has_emojis else "Clean"
}
]
}Save to <test-case>/<config>/grading.json.
Grading Schema
{
"expectations": [
{
"text": "Assertion description",
"passed": true,
"evidence": "Supporting details"
}
]
}Field requirements:
text: Human-readable assertion namepassed: Boolean resultevidence: Why it passed/failed
Phase 5: Aggregate Results
Use the bundled aggregation script:
python scripts/aggregate_benchmark.py \
--workspace workspace/iteration-1 \
--configs baseline treatment \
--test-name "emoji-removal"This generates:
benchmark.json- Machine-readable metricsbenchmark.md- Human-readable report
Benchmark Contents
Pass Rate: Percentage of assertions passed per configuration Token Usage: Mean ± stddev across test cases Execution Time: Mean ± stddev across test cases Delta: Difference between configurations (treatment - baseline)
Manual Aggregation
If you need to aggregate manually:
import json, statistics
configs = {}
for config in ['baseline', 'treatment']:
grades, tokens, times = [], [], []
for test_case in test_cases:
# Load grading
with open(f"{test_case}/{config}/grading.json") as f:
g = json.load(f)
passed = sum(1 for e in g['expectations'] if e['passed'])
total = len(g['expectations'])
grades.append(passed / total)
# Load timing
with open(f"{test_case}/{config}/timing.json") as f:
t = json.load(f)
tokens.append(t['total_tokens'])
times.append(t['total_duration_seconds'])
configs[config] = {
'pass_rate': {'mean': statistics.mean(grades), 'values': grades},
'tokens': {'mean': statistics.mean(tokens), 'stddev': statistics.stdev(tokens) if len(tokens) > 1 else 0},
'time': {'mean': statistics.mean(times), 'stddev': statistics.stdev(times) if len(times) > 1 else 0}
}
# Calculate deltas
delta = {
'pass_rate': configs['treatment']['pass_rate']['mean'] - configs['baseline']['pass_rate']['mean'],
'tokens': configs['treatment']['tokens']['mean'] - configs['baseline']['tokens']['mean'],
'time': configs['treatment']['time']['mean'] - configs['baseline']['time']['mean']
}Reporting Results
After aggregation, present findings to the user:
Summary Format
# <Test Name> - Benchmark Results
## Summary
- Test Cases: N
- Configurations: baseline vs treatment
## Results
### Pass Rate
- Baseline: X.X%
- Treatment: Y.Y%
- Delta: +Z.Z% (better/worse/same)
### Token Usage
- Baseline: X,XXX ± YYY tokens
- Treatment: X,XXX ± YYY tokens
- Delta: +Z,ZZZ tokens (X% increase/decrease)
### Execution Time
- Baseline: XX.X ± Y.Y seconds
- Treatment: XX.X ± Y.Y seconds
- Delta: +Z.Z seconds (X% faster/slower)
## Analysis
<Your interpretation of the results>
## Recommendation
<What the data suggests - e.g., "Treatment approach is 15% faster with same quality" or "Baseline is simpler and equally effective">Advanced: Multi-Configuration Testing
For 3+ configurations (e.g., baseline vs approach-A vs approach-B vs approach-C):
1. Spawn N×M agents (N test cases × M configurations) 2. Collect metrics for all 3. Grade all outputs 4. Aggregate with pairwise comparisons:
- Compare each treatment to baseline
- Optionally compare treatments to each other
5. Report best performer with statistical significance
Best Practices
Choosing Test Cases
Good test cases:
- Representative of real-world usage
- Test different aspects (edge cases, common cases, stress tests)
- Have clear success criteria
Poor test cases:
- Too similar to each other (test the same thing)
- Trivial (any approach works)
- Ambiguous success criteria
Fair Comparisons
Ensure apples-to-apples:
- Same prompt for all configurations
- Same input files
- Same model (unless testing model differences)
- Run truly in parallel (not sequentially)
Statistical Rigor
For small sample sizes (2-3 test cases):
- Report individual results, not just averages
- Be cautious about generalizing
- Consider variance (stddev) when interpreting means
For larger sample sizes (10+ test cases):
- Use statistical tests (t-tests, confidence intervals)
- Report effect sizes, not just p-values
- Consider practical significance vs statistical significance
Common Use Cases
Use Case 1: Skill Effectiveness Testing
Question: Does this skill improve results?
Setup:
- Baseline: Without skill
- Treatment: With skill
- Test cases: 3-5 representative tasks
Metrics:
- Quality: Pass rate on assertions
- Efficiency: Token usage, execution time
- Consistency: Stddev across test cases
Use Case 2: Prompt Optimization
Question: Which prompt variant works best?
Setup:
- Baseline: Original prompt
- Treatment-A: Variant with examples
- Treatment-B: Variant with reasoning steps
- Treatment-C: Variant with constraints
Metrics:
- Correctness: Did it produce the right output?
- Completeness: Did it address all requirements?
- Clarity: Is the output easy to understand?
Use Case 3: Refactoring Validation
Question: Did refactoring break anything?
Setup:
- Baseline: Old implementation
- Treatment: Refactored implementation
- Test cases: Comprehensive test suite
Metrics:
- Correctness: All tests pass
- Performance: Execution time
- Code quality: Complexity, readability (manual assessment)
Use Case 4: Model Comparison
Question: Which model is best for this task?
Setup:
- Baseline: Sonnet
- Treatment-A: Opus
- Treatment-B: Haiku
Metrics:
- Quality: Task-specific assertions
- Cost: Token usage × model pricing
- Speed: Execution time
Bundled Scripts
The skill includes helper scripts in scripts/:
grade_outputs.py
Programmatic grading helper:
python scripts/grade_outputs.py \
--output-dir <path-to-outputs> \
--assertions <assertions-json-file> \
--save-to <grading-json-path>aggregate_benchmark.py
Benchmark aggregation:
python scripts/aggregate_benchmark.py \
--workspace <iteration-dir> \
--configs baseline treatment \
--test-name "my-test"compare_outputs.py
Detailed diff generation for outputs:
python scripts/compare_outputs.py \
--baseline <baseline-output> \
--treatment <treatment-output> \
--format <text|json|md>Workflow Checklist
Use this checklist for each test run:
Setup Phase:
- [ ] Define test name and configurations
- [ ] Create workspace structure
- [ ] Define test cases with clear prompts
- [ ] Define assertions for grading
Execution Phase:
- [ ] Spawn all agents in parallel (single message, multiple Agent calls)
- [ ] Monitor agent completions
- [ ] Save timing.json for each completion immediately
Analysis Phase:
- [ ] Grade all outputs against assertions
- [ ] Save grading.json for each configuration
- [ ] Aggregate results into benchmark.json
- [ ] Generate benchmark.md report
Reporting Phase:
- [ ] Present summary to user
- [ ] Explain key findings
- [ ] Make recommendation based on data
Remember
The goal is to provide objective, data-driven comparisons that help the user make informed decisions. Focus on:
1. Fair tests - Same conditions for all configurations 2. Meaningful metrics - Measure what actually matters 3. Clear reporting - Present results understandably 4. Actionable insights - What should the user do with this data?
Avoid:
- Spawning agents sequentially (defeats the purpose of parallel testing)
- Forgetting to capture timing data (only available in task notification)
- Making claims without statistical backing
- Testing configurations that differ in multiple ways (can't isolate cause)
#!/usr/bin/env python3
"""
Aggregate test results into benchmark report.
Usage: python aggregate_benchmark.py --workspace iteration-1 --configs baseline treatment --test-name my-test
"""
import argparse
import json
import statistics
from pathlib import Path
from typing import Dict, List
def load_timing(path: Path) -> Dict:
"""Load timing data from timing.json."""
with open(path) as f:
return json.load(f)
def load_grading(path: Path) -> Dict:
"""Load grading data from grading.json."""
with open(path) as f:
return json.load(f)
def calculate_pass_rate(grading: Dict) -> float:
"""Calculate pass rate from grading data."""
expectations = grading.get('expectations', [])
if not expectations:
return 0.0
passed = sum(1 for exp in expectations if exp.get('passed', False))
return passed / len(expectations)
def aggregate_config(workspace: Path, config: str, test_cases: List[Path]) -> Dict:
"""Aggregate metrics for a single configuration across all test cases."""
pass_rates = []
tokens = []
times = []
for test_case_dir in test_cases:
config_dir = test_case_dir / config
# Load and aggregate grading
grading_file = config_dir / 'grading.json'
if grading_file.exists():
grading = load_grading(grading_file)
pass_rates.append(calculate_pass_rate(grading))
# Load and aggregate timing
timing_file = config_dir / 'timing.json'
if timing_file.exists():
timing = load_timing(timing_file)
tokens.append(timing['total_tokens'])
times.append(timing['total_duration_seconds'])
return {
'pass_rate': {
'mean': statistics.mean(pass_rates) if pass_rates else 0.0,
'values': pass_rates
},
'tokens': {
'mean': statistics.mean(tokens) if tokens else 0,
'stddev': statistics.stdev(tokens) if len(tokens) > 1 else 0,
'values': tokens
},
'time_seconds': {
'mean': statistics.mean(times) if times else 0,
'stddev': statistics.stdev(times) if len(times) > 1 else 0,
'values': times
}
}
def find_test_cases(workspace: Path) -> List[Path]:
"""Find all test case directories in workspace."""
test_cases = []
for item in workspace.iterdir():
if item.is_dir() and not item.name.startswith('.'):
# Check if it has config subdirectories
has_configs = any(
(item / d).is_dir()
for d in ['baseline', 'treatment']
)
if has_configs:
test_cases.append(item)
return sorted(test_cases)
def generate_markdown_report(benchmark: Dict, test_name: str) -> str:
"""Generate human-readable markdown report."""
configs = benchmark['configurations']
baseline = next((c for c in configs if 'baseline' in c['name'].lower()), configs[0])
treatments = [c for c in configs if c != baseline]
report = [f"# {test_name} - Benchmark Results\n"]
report.append(f"**Test Cases:** {len(baseline['pass_rate']['values'])}")
report.append(f"**Configurations:** {len(configs)}\n")
report.append("## Results\n")
# Pass Rate
report.append("### Pass Rate")
report.append(f"- **{baseline['name']}:** {baseline['pass_rate']['mean']:.1%}")
for treatment in treatments:
delta = treatment['pass_rate']['mean'] - baseline['pass_rate']['mean']
report.append(f"- **{treatment['name']}:** {treatment['pass_rate']['mean']:.1%} (delta: {delta:+.1%})")
report.append("")
# Token Usage
report.append("### Token Usage")
report.append(
f"- **{baseline['name']}:** {baseline['tokens']['mean']:.0f} ± {baseline['tokens']['stddev']:.0f}"
)
for treatment in treatments:
delta = treatment['tokens']['mean'] - baseline['tokens']['mean']
pct = (delta / baseline['tokens']['mean'] * 100) if baseline['tokens']['mean'] > 0 else 0
report.append(
f"- **{treatment['name']}:** {treatment['tokens']['mean']:.0f} ± {treatment['tokens']['stddev']:.0f} "
f"(delta: {delta:+.0f}, {pct:+.1f}%)"
)
report.append("")
# Execution Time
report.append("### Execution Time")
report.append(
f"- **{baseline['name']}:** {baseline['time_seconds']['mean']:.1f} ± {baseline['time_seconds']['stddev']:.1f}s"
)
for treatment in treatments:
delta = treatment['time_seconds']['mean'] - baseline['time_seconds']['mean']
pct = (delta / baseline['time_seconds']['mean'] * 100) if baseline['time_seconds']['mean'] > 0 else 0
report.append(
f"- **{treatment['name']}:** {treatment['time_seconds']['mean']:.1f} ± {treatment['time_seconds']['stddev']:.1f}s "
f"(delta: {delta:+.1f}s, {pct:+.1f}%)"
)
report.append("")
return "\n".join(report)
def main():
parser = argparse.ArgumentParser(description="Aggregate benchmark results")
parser.add_argument('--workspace', type=Path, required=True, help="Workspace directory (e.g., iteration-1)")
parser.add_argument('--configs', nargs='+', required=True, help="Configuration names (e.g., baseline treatment)")
parser.add_argument('--test-name', required=True, help="Name of test")
args = parser.parse_args()
if not args.workspace.exists():
print(f"Error: Workspace {args.workspace} does not exist")
return 1
# Find test cases
test_cases = find_test_cases(args.workspace)
if not test_cases:
print(f"Error: No test cases found in {args.workspace}")
return 1
print(f"Found {len(test_cases)} test cases: {[tc.name for tc in test_cases]}")
# Aggregate each configuration
configurations = []
for config in args.configs:
print(f"Aggregating {config}...")
config_data = aggregate_config(args.workspace, config, test_cases)
config_data['name'] = config
configurations.append(config_data)
# Create benchmark
benchmark = {
'test_name': args.test_name,
'test_cases': [tc.name for tc in test_cases],
'configurations': configurations
}
# Calculate deltas (vs first config, assumed to be baseline)
if len(configurations) > 1:
baseline = configurations[0]
deltas = []
for treatment in configurations[1:]:
deltas.append({
'config': treatment['name'],
'pass_rate': treatment['pass_rate']['mean'] - baseline['pass_rate']['mean'],
'tokens': treatment['tokens']['mean'] - baseline['tokens']['mean'],
'time_seconds': treatment['time_seconds']['mean'] - baseline['time_seconds']['mean']
})
benchmark['deltas'] = deltas
# Save benchmark.json
benchmark_json = args.workspace / 'benchmark.json'
with open(benchmark_json, 'w') as f:
json.dump(benchmark, f, indent=2)
print(f"Saved benchmark.json to {benchmark_json}")
# Generate markdown report
report = generate_markdown_report(benchmark, args.test_name)
benchmark_md = args.workspace / 'benchmark.md'
with open(benchmark_md, 'w') as f:
f.write(report)
print(f"Saved benchmark.md to {benchmark_md}")
print("\nBenchmark generated successfully!")
return 0
if __name__ == '__main__':
exit(main())
Related skills
FAQ
How does it run agents in parallel?
It spawns all agents in a single message using multiple Agent tool calls so they run truly in parallel.
What metrics does it collect?
Per run it captures total_tokens and duration_ms from the task notification, then grades outputs against assertions and aggregates a benchmark.