@dev-sergeev/datalab-kit 0.1.5 → 0.1.6

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 (51) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +23 -1
  3. package/THIRD_PARTY_NOTICES.md +3 -0
  4. package/dist/extension.js +2 -0
  5. package/dist/skills.d.ts +2 -0
  6. package/dist/skills.js +54 -0
  7. package/package.json +36 -9
  8. package/skills/init/SKILL.md +31 -0
  9. package/skills/skill-creator/LICENSE.txt +202 -0
  10. package/skills/skill-creator/SKILL.md +610 -0
  11. package/skills/skill-creator/agents/analyzer.md +291 -0
  12. package/skills/skill-creator/agents/comparator.md +213 -0
  13. package/skills/skill-creator/agents/grader.md +234 -0
  14. package/skills/skill-creator/assets/eval_review.html +146 -0
  15. package/skills/skill-creator/eval-viewer/generate_review.py +453 -0
  16. package/skills/skill-creator/eval-viewer/viewer.html +1329 -0
  17. package/skills/skill-creator/references/schemas.md +521 -0
  18. package/skills/skill-creator/scripts/aggregate_benchmark.py +415 -0
  19. package/skills/skill-creator/scripts/generate_report.py +329 -0
  20. package/skills/skill-creator/scripts/improve_description.py +225 -0
  21. package/skills/skill-creator/scripts/package_skill.py +136 -0
  22. package/skills/skill-creator/scripts/pi_runtime.py +177 -0
  23. package/skills/skill-creator/scripts/quick_validate.py +107 -0
  24. package/skills/skill-creator/scripts/run_eval.py +215 -0
  25. package/skills/skill-creator/scripts/run_loop.py +334 -0
  26. package/skills/skill-creator/scripts/utils.py +41 -0
  27. package/node_modules/node-addon-api/common 2.gypi +0 -21
  28. package/node_modules/node-addon-api/except 2.gypi +0 -25
  29. package/node_modules/node-addon-api/napi 2.h +0 -3463
  30. package/node_modules/node-addon-api/napi-inl 2.h +0 -7250
  31. package/node_modules/node-addon-api/napi-inl.deprecated 2.h +0 -186
  32. package/node_modules/node-addon-api/node_addon_api 2.gyp +0 -42
  33. package/node_modules/node-addon-api/node_api 2.gyp +0 -9
  34. package/node_modules/node-addon-api/noexcept 2.gypi +0 -26
  35. package/node_modules/pi-toggle-skills/toggle-skills 2.ts +0 -316
  36. package/node_modules/pi-toggle-skills/vitest.config 2.ts +0 -15
  37. package/node_modules/sprintf-js/.npmignore 2 +0 -1
  38. package/node_modules/sprintf-js/LICENSE 2 +0 -24
  39. package/node_modules/sprintf-js/README 2.md +0 -88
  40. package/node_modules/sprintf-js/bower 2.json +0 -14
  41. package/node_modules/sprintf-js/gruntfile 2.js +0 -36
  42. package/node_modules/sprintf-js/package 2.json +0 -22
  43. package/node_modules/tree-sitter-bash/tree-sitter-bash 2.wasm +0 -0
  44. package/node_modules/typebox/license 2 +0 -23
  45. package/node_modules/typebox/package 2.json +0 -87
  46. package/node_modules/typebox/readme 2.md +0 -370
  47. package/node_modules/zod/compile 2.cjs +0 -96
  48. package/node_modules/zod/compile 2.js +0 -52
  49. package/node_modules/zod/index 2.cjs +0 -52
  50. package/node_modules/zod/index 2.js +0 -7
  51. /package/{node_modules/node-addon-api/nothing 2.c → skills/skill-creator/scripts/__init__.py} +0 -0
@@ -0,0 +1,415 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Aggregate individual run results into benchmark summary statistics.
4
+
5
+ Reads grading.json files from run directories and produces:
6
+ - run_summary with mean, stddev, min, max for each metric
7
+ - delta between with_skill and without_skill configurations
8
+
9
+ Usage:
10
+ python aggregate_benchmark.py <benchmark_dir>
11
+
12
+ Example:
13
+ python aggregate_benchmark.py benchmarks/2026-01-15T10-30-00/
14
+
15
+ The script supports two directory layouts:
16
+
17
+ Workspace layout (from skill-creator iterations):
18
+ <benchmark_dir>/
19
+ └── eval-N/
20
+ ├── with_skill/
21
+ │ ├── run-1/grading.json
22
+ │ └── run-2/grading.json
23
+ └── without_skill/
24
+ ├── run-1/grading.json
25
+ └── run-2/grading.json
26
+
27
+ Legacy layout (with runs/ subdirectory):
28
+ <benchmark_dir>/
29
+ └── runs/
30
+ └── eval-N/
31
+ ├── with_skill/
32
+ │ └── run-1/grading.json
33
+ └── without_skill/
34
+ └── run-1/grading.json
35
+
36
+ Changed by Datalab Kit: report observed repetition counts and preserve absent Pi
37
+ telemetry as null, never substitute character counts for tokens. Upstream: anthropics/skills
38
+ dbd4588f9e1033efb41dad4bef2f7947c8993d44 (Apache-2.0).
39
+ """
40
+
41
+ import argparse
42
+ import json
43
+ import math
44
+ import sys
45
+ from datetime import datetime, timezone
46
+ from collections import Counter
47
+ from pathlib import Path
48
+
49
+
50
+ def calculate_stats(values: list[float]) -> dict:
51
+ """Calculate mean, stddev, min, max for a list of values."""
52
+ if not values:
53
+ return {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0}
54
+
55
+ n = len(values)
56
+ mean = sum(values) / n
57
+
58
+ if n > 1:
59
+ variance = sum((x - mean) ** 2 for x in values) / (n - 1)
60
+ stddev = math.sqrt(variance)
61
+ else:
62
+ stddev = 0.0
63
+
64
+ return {
65
+ "mean": round(mean, 4),
66
+ "stddev": round(stddev, 4),
67
+ "min": round(min(values), 4),
68
+ "max": round(max(values), 4)
69
+ }
70
+
71
+
72
+ def load_run_results(benchmark_dir: Path) -> dict:
73
+ """
74
+ Load all run results from a benchmark directory.
75
+
76
+ Returns dict keyed by config name (e.g. "with_skill"/"without_skill",
77
+ or "new_skill"/"old_skill"), each containing a list of run results.
78
+ """
79
+ # Support both layouts: eval dirs directly under benchmark_dir, or under runs/
80
+ runs_dir = benchmark_dir / "runs"
81
+ if runs_dir.exists():
82
+ search_dir = runs_dir
83
+ elif list(benchmark_dir.glob("eval-*")):
84
+ search_dir = benchmark_dir
85
+ else:
86
+ print(f"No eval directories found in {benchmark_dir} or {benchmark_dir / 'runs'}")
87
+ return {}
88
+
89
+ results: dict[str, list] = {}
90
+
91
+ for eval_idx, eval_dir in enumerate(sorted(search_dir.glob("eval-*"))):
92
+ metadata_path = eval_dir / "eval_metadata.json"
93
+ if metadata_path.exists():
94
+ try:
95
+ with open(metadata_path) as mf:
96
+ eval_id = json.load(mf).get("eval_id", eval_idx)
97
+ except (json.JSONDecodeError, OSError):
98
+ eval_id = eval_idx
99
+ else:
100
+ try:
101
+ eval_id = int(eval_dir.name.split("-")[1])
102
+ except ValueError:
103
+ eval_id = eval_idx
104
+
105
+ # Discover config directories dynamically rather than hardcoding names
106
+ for config_dir in sorted(eval_dir.iterdir()):
107
+ if not config_dir.is_dir():
108
+ continue
109
+ # Skip non-config directories (inputs, outputs, etc.)
110
+ if not list(config_dir.glob("run-*")):
111
+ continue
112
+ config = config_dir.name
113
+ if config not in results:
114
+ results[config] = []
115
+
116
+ for run_dir in sorted(config_dir.glob("run-*")):
117
+ run_number = int(run_dir.name.split("-")[1])
118
+ grading_file = run_dir / "grading.json"
119
+
120
+ if not grading_file.exists():
121
+ print(f"Warning: grading.json not found in {run_dir}")
122
+ continue
123
+
124
+ try:
125
+ with open(grading_file) as f:
126
+ grading = json.load(f)
127
+ except json.JSONDecodeError as e:
128
+ print(f"Warning: Invalid JSON in {grading_file}: {e}")
129
+ continue
130
+
131
+ # Extract metrics
132
+ result = {
133
+ "eval_id": eval_id,
134
+ "run_number": run_number,
135
+ "pass_rate": grading.get("summary", {}).get("pass_rate", 0.0),
136
+ "passed": grading.get("summary", {}).get("passed", 0),
137
+ "failed": grading.get("summary", {}).get("failed", 0),
138
+ "total": grading.get("summary", {}).get("total", 0),
139
+ }
140
+
141
+ # Extract timing — check grading.json first, then sibling timing.json
142
+ timing = dict(grading.get("timing", {}))
143
+ timing_file = run_dir / "timing.json"
144
+ if timing_file.exists():
145
+ with open(timing_file) as tf:
146
+ recorded_timing = json.load(tf)
147
+ timing = {**recorded_timing, **timing}
148
+ result["time_seconds"] = timing.get("total_duration_seconds")
149
+ result["tokens"] = timing.get("total_tokens")
150
+
151
+ # Token counts must be provider-reported, not output characters.
152
+ metrics = grading.get("execution_metrics", {})
153
+ result["tool_calls"] = metrics.get("total_tool_calls", 0)
154
+ if result["tokens"] is None:
155
+ result["tokens"] = metrics.get("total_tokens")
156
+ result["errors"] = metrics.get("errors_encountered", 0)
157
+
158
+ # Extract expectations — viewer requires fields: text, passed, evidence
159
+ raw_expectations = grading.get("expectations", [])
160
+ for exp in raw_expectations:
161
+ if "text" not in exp or "passed" not in exp:
162
+ print(f"Warning: expectation in {grading_file} missing required fields (text, passed, evidence): {exp}")
163
+ result["expectations"] = raw_expectations
164
+
165
+ # Extract notes from user_notes_summary
166
+ notes_summary = grading.get("user_notes_summary", {})
167
+ notes = []
168
+ notes.extend(notes_summary.get("uncertainties", []))
169
+ notes.extend(notes_summary.get("needs_review", []))
170
+ notes.extend(notes_summary.get("workarounds", []))
171
+ result["notes"] = notes
172
+
173
+ results[config].append(result)
174
+
175
+ return results
176
+
177
+
178
+ def aggregate_results(results: dict) -> dict:
179
+ """
180
+ Aggregate run results into summary statistics.
181
+
182
+ Returns run_summary with stats for each configuration and delta.
183
+ """
184
+ run_summary = {}
185
+ configs = list(results.keys())
186
+
187
+ for config in configs:
188
+ runs = results.get(config, [])
189
+
190
+ if not runs:
191
+ run_summary[config] = {
192
+ "pass_rate": {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0},
193
+ "time_seconds": None,
194
+ "tokens": None,
195
+ }
196
+ continue
197
+
198
+ pass_rates = [r["pass_rate"] for r in runs]
199
+ times = [r["time_seconds"] for r in runs if r["time_seconds"] is not None]
200
+ tokens = [r["tokens"] for r in runs if r.get("tokens") is not None]
201
+
202
+ run_summary[config] = {
203
+ "pass_rate": calculate_stats(pass_rates),
204
+ "time_seconds": calculate_stats(times) if times else None,
205
+ "tokens": calculate_stats(tokens) if tokens else None,
206
+ }
207
+
208
+ # Calculate delta between the first two configs (if two exist)
209
+ if len(configs) >= 2:
210
+ primary = run_summary.get(configs[0], {})
211
+ baseline = run_summary.get(configs[1], {})
212
+ else:
213
+ primary = run_summary.get(configs[0], {}) if configs else {}
214
+ baseline = {}
215
+
216
+ delta_pass_rate = primary.get("pass_rate", {}).get("mean", 0) - baseline.get("pass_rate", {}).get("mean", 0)
217
+ def metric_delta(metric, precision):
218
+ a, b = primary.get(metric), baseline.get(metric)
219
+ return f"{a['mean'] - b['mean']:+.{precision}f}" if a and b else None
220
+
221
+ run_summary["delta"] = {
222
+ "pass_rate": f"{delta_pass_rate:+.2f}",
223
+ "time_seconds": metric_delta("time_seconds", 1),
224
+ "tokens": metric_delta("tokens", 0),
225
+ }
226
+
227
+ return run_summary
228
+
229
+
230
+ def generate_benchmark(benchmark_dir: Path, skill_name: str = "", skill_path: str = "") -> dict:
231
+ """
232
+ Generate complete benchmark.json from run results.
233
+ """
234
+ results = load_run_results(benchmark_dir)
235
+ run_summary = aggregate_results(results)
236
+
237
+ # Build runs array for benchmark.json
238
+ runs = []
239
+ for config in results:
240
+ for result in results[config]:
241
+ runs.append({
242
+ "eval_id": result["eval_id"],
243
+ "configuration": config,
244
+ "run_number": result["run_number"],
245
+ "result": {
246
+ "pass_rate": result["pass_rate"],
247
+ "passed": result["passed"],
248
+ "failed": result["failed"],
249
+ "total": result["total"],
250
+ "time_seconds": result["time_seconds"],
251
+ "tokens": result.get("tokens"),
252
+ "tool_calls": result.get("tool_calls", 0),
253
+ "errors": result.get("errors", 0)
254
+ },
255
+ "expectations": result["expectations"],
256
+ "notes": result["notes"]
257
+ })
258
+
259
+ # Determine eval IDs from results
260
+ eval_ids = sorted(set(
261
+ r["eval_id"]
262
+ for config in results.values()
263
+ for r in config
264
+ ))
265
+ repetition_counts = set(Counter(
266
+ (run["eval_id"], run["configuration"]) for run in runs
267
+ ).values())
268
+ repetitions = next(iter(repetition_counts)) if len(repetition_counts) == 1 else None
269
+
270
+ benchmark = {
271
+ "metadata": {
272
+ "skill_name": skill_name or None,
273
+ "skill_path": skill_path or None,
274
+ "executor_model": None,
275
+ "analyzer_model": None,
276
+ "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
277
+ "evals_run": eval_ids,
278
+ "runs_per_configuration": repetitions
279
+ },
280
+ "runs": runs,
281
+ "run_summary": run_summary,
282
+ "notes": [] # To be filled by analyzer
283
+ }
284
+
285
+ return benchmark
286
+
287
+
288
+ def generate_markdown(benchmark: dict) -> str:
289
+ """Generate human-readable benchmark.md from benchmark data."""
290
+ metadata = benchmark["metadata"]
291
+ run_summary = benchmark["run_summary"]
292
+
293
+ # Determine config names (excluding "delta")
294
+ configs = [k for k in run_summary if k != "delta"]
295
+ config_a = configs[0] if len(configs) >= 1 else "config_a"
296
+ config_b = configs[1] if len(configs) >= 2 else "config_b"
297
+ label_a = config_a.replace("_", " ").title()
298
+ label_b = config_b.replace("_", " ").title()
299
+
300
+ repetitions = metadata["runs_per_configuration"]
301
+ run_count_label = f"{repetitions} runs each per configuration" if repetitions is not None else "see per-eval run counts"
302
+ lines = [
303
+ f"# Skill Benchmark: {metadata['skill_name']}",
304
+ "",
305
+ f"**Model**: {metadata['executor_model']}",
306
+ f"**Date**: {metadata['timestamp']}",
307
+ f"**Evals**: {', '.join(map(str, metadata['evals_run']))} ({run_count_label})",
308
+ "",
309
+ "## Summary",
310
+ "",
311
+ f"| Metric | {label_a} | {label_b} | Delta |",
312
+ "|--------|------------|---------------|-------|",
313
+ ]
314
+
315
+ a_summary = run_summary.get(config_a, {})
316
+ b_summary = run_summary.get(config_b, {})
317
+ delta = run_summary.get("delta", {})
318
+
319
+ # Format pass rate
320
+ a_pr = a_summary.get("pass_rate", {})
321
+ b_pr = b_summary.get("pass_rate", {})
322
+ lines.append(f"| Pass Rate | {a_pr.get('mean', 0)*100:.0f}% ± {a_pr.get('stddev', 0)*100:.0f}% | {b_pr.get('mean', 0)*100:.0f}% ± {b_pr.get('stddev', 0)*100:.0f}% | {delta.get('pass_rate', '—')} |")
323
+
324
+ def format_metric(summary, metric, precision, unit=""):
325
+ stat = summary.get(metric)
326
+ if not stat:
327
+ return "—"
328
+ return f"{stat['mean']:.{precision}f}{unit} ± {stat['stddev']:.{precision}f}{unit}"
329
+
330
+ a_time = format_metric(a_summary, "time_seconds", 1, "s")
331
+ b_time = format_metric(b_summary, "time_seconds", 1, "s")
332
+ time_delta = delta.get("time_seconds")
333
+ lines.append(f"| Time | {a_time} | {b_time} | {time_delta + 's' if time_delta is not None else '—'} |")
334
+
335
+ a_tokens = format_metric(a_summary, "tokens", 0)
336
+ b_tokens = format_metric(b_summary, "tokens", 0)
337
+ lines.append(f"| Tokens | {a_tokens} | {b_tokens} | {delta.get('tokens') or '—'} |")
338
+
339
+ # Notes section
340
+ if benchmark.get("notes"):
341
+ lines.extend([
342
+ "",
343
+ "## Notes",
344
+ ""
345
+ ])
346
+ for note in benchmark["notes"]:
347
+ lines.append(f"- {note}")
348
+
349
+ return "\n".join(lines)
350
+
351
+
352
+ def main():
353
+ parser = argparse.ArgumentParser(
354
+ description="Aggregate benchmark run results into summary statistics"
355
+ )
356
+ parser.add_argument(
357
+ "benchmark_dir",
358
+ type=Path,
359
+ help="Path to the benchmark directory"
360
+ )
361
+ parser.add_argument(
362
+ "--skill-name",
363
+ default="",
364
+ help="Name of the skill being benchmarked"
365
+ )
366
+ parser.add_argument(
367
+ "--skill-path",
368
+ default="",
369
+ help="Path to the skill being benchmarked"
370
+ )
371
+ parser.add_argument(
372
+ "--output", "-o",
373
+ type=Path,
374
+ help="Output path for benchmark.json (default: <benchmark_dir>/benchmark.json)"
375
+ )
376
+
377
+ args = parser.parse_args()
378
+
379
+ if not args.benchmark_dir.exists():
380
+ print(f"Directory not found: {args.benchmark_dir}")
381
+ sys.exit(1)
382
+
383
+ # Generate benchmark
384
+ benchmark = generate_benchmark(args.benchmark_dir, args.skill_name, args.skill_path)
385
+
386
+ # Determine output paths
387
+ output_json = args.output or (args.benchmark_dir / "benchmark.json")
388
+ output_md = output_json.with_suffix(".md")
389
+
390
+ # Write benchmark.json
391
+ with open(output_json, "w") as f:
392
+ json.dump(benchmark, f, indent=2)
393
+ print(f"Generated: {output_json}")
394
+
395
+ # Write benchmark.md
396
+ markdown = generate_markdown(benchmark)
397
+ with open(output_md, "w") as f:
398
+ f.write(markdown)
399
+ print(f"Generated: {output_md}")
400
+
401
+ # Print summary
402
+ run_summary = benchmark["run_summary"]
403
+ configs = [k for k in run_summary if k != "delta"]
404
+ delta = run_summary.get("delta", {})
405
+
406
+ print(f"\nSummary:")
407
+ for config in configs:
408
+ pr = run_summary[config]["pass_rate"]["mean"]
409
+ label = config.replace("_", " ").title()
410
+ print(f" {label}: {pr*100:.1f}% pass rate")
411
+ print(f" Delta: {delta.get('pass_rate', '—')}")
412
+
413
+
414
+ if __name__ == "__main__":
415
+ main()