chill-agent 1.4.0 → 1.6.0
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/.agents/rules/ponytail.md +30 -0
- package/.claude-plugin/contrast-mcp-launcher.mjs +31 -0
- package/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +8 -3
- package/.cline-plugin/plugin.js +6 -0
- package/.clinerules/ponytail.md +30 -0
- package/.codex-plugin/plugin.json +4 -3
- package/.cursor/rules/ponytail.mdc +36 -0
- package/.cursor-plugin/marketplace.json +16 -0
- package/.cursor-plugin/plugin.json +2 -2
- package/.devin-plugin/plugin.json +2 -2
- package/.grok-plugin/marketplace.json +16 -0
- package/.kimi-plugin/plugin.json +11 -29
- package/.kiro/steering/ponytail.md +35 -0
- package/.omp-plugin/marketplace.json +15 -0
- package/.openclaw/skills/ponytail/SKILL.md +47 -0
- package/.openclaw/skills/ponytail-audit/SKILL.md +100 -0
- package/.openclaw/skills/ponytail-debt/SKILL.md +43 -0
- package/.openclaw/skills/ponytail-gain/SKILL.md +51 -0
- package/.openclaw/skills/ponytail-help/SKILL.md +76 -0
- package/.openclaw/skills/ponytail-review/SKILL.md +98 -0
- package/.opencode/command/ponytail-audit.md +5 -0
- package/.opencode/command/ponytail-debt.md +5 -0
- package/.opencode/command/ponytail-gain.md +5 -0
- package/.opencode/command/ponytail-help.md +5 -0
- package/.opencode/command/ponytail-review.md +5 -0
- package/.opencode/command/ponytail.md +5 -0
- package/.opencode/plugins/index.js +4 -0
- package/.opencode/plugins/ponytail-frontmatter.cjs +63 -0
- package/.opencode/plugins/ponytail.mjs +206 -0
- package/.qoder-plugin/plugin.json +12 -26
- package/.windsurf/rules/ponytail.md +30 -0
- package/AGENTS.md +32 -0
- package/__init__.py +228 -0
- package/assets/antislop-banner.png +0 -0
- package/assets/band/band.png +0 -0
- package/assets/band/board.png +0 -0
- package/assets/benchmark-3model.svg +21 -0
- package/assets/benchmark-agentic.svg +62 -0
- package/assets/codex-icon.png +0 -0
- package/assets/compare/code/after.webp +0 -0
- package/assets/compare/code/before.webp +0 -0
- package/assets/compare/text/after.webp +0 -0
- package/assets/compare/text/before.webp +0 -0
- package/assets/compare/ui/w-all.webp +0 -0
- package/assets/compare/ui/w-antislop.webp +0 -0
- package/assets/compare/ui/w-design.webp +0 -0
- package/assets/compare/ui/without.webp +0 -0
- package/assets/logo-dark.png +0 -0
- package/assets/logo-dark.svg +115 -0
- package/assets/logo-greenpt-dark.svg +27 -0
- package/assets/logo-greenpt.svg +27 -0
- package/assets/logo.png +0 -0
- package/assets/photoreunion-logo-dark.svg +4 -0
- package/assets/photoreunion-logo-light.svg +4 -0
- package/assets/profile.png +0 -0
- package/assets/retriever-logo-dark.svg +4 -0
- package/assets/retriever-logo-light.svg +4 -0
- package/assets/stories-banner.png +0 -0
- package/assets/v5/audit-es.png +0 -0
- package/assets/v5/audit-ja.png +0 -0
- package/assets/v5/audit-ko.png +0 -0
- package/assets/v5/audit-zh-CN.png +0 -0
- package/assets/v5/audit.png +0 -0
- package/assets/v5/beforeafter-es.png +0 -0
- package/assets/v5/beforeafter-ja.png +0 -0
- package/assets/v5/beforeafter-ko.png +0 -0
- package/assets/v5/beforeafter-zh-CN.png +0 -0
- package/assets/v5/beforeafter.png +0 -0
- package/assets/v5/chart-es.png +0 -0
- package/assets/v5/chart-ja.png +0 -0
- package/assets/v5/chart-ko.png +0 -0
- package/assets/v5/chart-zh-CN.png +0 -0
- package/assets/v5/chart.png +0 -0
- package/assets/v5/hero-es.jpg +0 -0
- package/assets/v5/hero-ja.jpg +0 -0
- package/assets/v5/hero-ko.jpg +0 -0
- package/assets/v5/hero-zh-CN.jpg +0 -0
- package/assets/v5/hero.jpg +0 -0
- package/assets/v5/ladder-es.png +0 -0
- package/assets/v5/ladder-ja.png +0 -0
- package/assets/v5/ladder-ko.png +0 -0
- package/assets/v5/ladder-zh-CN.png +0 -0
- package/assets/v5/ladder.png +0 -0
- package/assets/v5/review-es.png +0 -0
- package/assets/v5/review-ja.png +0 -0
- package/assets/v5/review-ko.png +0 -0
- package/assets/v5/review-zh-CN.png +0 -0
- package/assets/v5/review.png +0 -0
- package/assets/v5/tests-es.png +0 -0
- package/assets/v5/tests-ja.png +0 -0
- package/assets/v5/tests-ko.png +0 -0
- package/assets/v5/tests-zh-CN.png +0 -0
- package/assets/v5/tests.png +0 -0
- package/assets/waitlist-banner-es.png +0 -0
- package/assets/waitlist-banner-ja.png +0 -0
- package/assets/waitlist-banner-ko.png +0 -0
- package/assets/waitlist-banner-zh-CN.png +0 -0
- package/assets/waitlist-banner.png +0 -0
- package/benchmarks/.env.example +2 -0
- package/benchmarks/agentic/README.md +201 -0
- package/benchmarks/agentic/answer.py +288 -0
- package/benchmarks/agentic/compare.py +93 -0
- package/benchmarks/agentic/complete.py +178 -0
- package/benchmarks/agentic/judge.py +198 -0
- package/benchmarks/agentic/run.py +543 -0
- package/benchmarks/agentic/tasks.py +1544 -0
- package/benchmarks/arms/baseline.js +2 -0
- package/benchmarks/arms/caveman-SKILL.md +67 -0
- package/benchmarks/arms/caveman.js +8 -0
- package/benchmarks/arms/ponytail.js +8 -0
- package/benchmarks/behavior.js +54 -0
- package/benchmarks/behavior.yaml +40 -0
- package/benchmarks/correctness.js +291 -0
- package/benchmarks/loc.js +17 -19
- package/benchmarks/promptfooconfig.gemini.yaml +32 -0
- package/benchmarks/promptfooconfig.gpt-newest.yaml +33 -0
- package/benchmarks/promptfooconfig.yaml +41 -0
- package/benchmarks/results/2026-06-12-caveman-vs-ponytail.md +71 -0
- package/benchmarks/results/2026-06-12-v4-hardening-vs-caveman.md +125 -0
- package/benchmarks/results/2026-06-15-llama3.2-local.md +82 -0
- package/benchmarks/results/2026-06-16-correctness-gate-fix.md +113 -0
- package/benchmarks/results/2026-06-16-robustness-audit.md +135 -0
- package/benchmarks/results/2026-06-17-agentic-safety.md +164 -0
- package/benchmarks/results/2026-06-17-cost-verification.md +93 -0
- package/benchmarks/results/2026-06-18-agentic.md +219 -0
- package/benchmarks/results/2026-06-22-issue-245-217-comprehension.md +98 -0
- package/benchmarks/results/2026-10-07-agentic.md +186 -0
- package/cli/index.mjs +241 -0
- package/cli/lib/banner.mjs +21 -0
- package/cli/lib/install.mjs +498 -0
- package/cli/lib/settings.mjs +48 -0
- package/cli/package-lock.json +87 -0
- package/cli/package.json +42 -0
- package/cli/scripts/smoke-test.mjs +95 -0
- package/cli/scripts/smoke-worker.mjs +411 -0
- package/cli/scripts/sync-skills.mjs +81 -0
- package/commands/ponytail-audit.toml +2 -0
- package/commands/ponytail-debt.toml +2 -0
- package/commands/ponytail-gain.toml +2 -0
- package/commands/ponytail-help.toml +2 -0
- package/commands/ponytail-review.toml +2 -0
- package/commands/ponytail.toml +2 -0
- package/examples/email-validation.md +156 -0
- package/examples/group-by.md +35 -0
- package/examples/infinite-scroll.md +58 -0
- package/examples/number-formatting.md +37 -0
- package/examples/rate-limit.md +272 -0
- package/examples/react-countdown.md +390 -0
- package/examples/url-params.md +41 -0
- package/gemini-extension.json +2 -2
- package/hooks/band/board.ts +560 -0
- package/hooks/band/logo.ts +3 -0
- package/hooks/band/metrics.ts +311 -0
- package/hooks/band/register.tsx +495 -0
- package/hooks/band/types.d.ts +34 -0
- package/hooks/chill-statusline.ps1 +1 -1
- package/hooks/claude-band.json +6 -0
- package/hooks/claude-codex-hooks.json +34 -4
- package/hooks/copilot-hooks.json +12 -2
- package/hooks/cursor-hooks.json +2 -2
- package/hooks/ponytail-activate.js +201 -0
- package/hooks/ponytail-band-import.js +274 -0
- package/hooks/ponytail-config.js +175 -0
- package/hooks/ponytail-instructions.js +98 -0
- package/hooks/ponytail-map.js +111 -0
- package/hooks/ponytail-mode-tracker.js +216 -0
- package/hooks/ponytail-runtime.js +182 -0
- package/hooks/ponytail-statusline.ps1 +24 -0
- package/hooks/ponytail-statusline.sh +18 -0
- package/hooks/ponytail-subagent.js +94 -0
- package/hooks/qoder-hooks.json +19 -4
- package/i18n/README.ko.md +230 -0
- package/package.json +17 -6
- package/pi-extension/index.js +254 -0
- package/pi-extension/package.json +8 -0
- package/pi-extension/test/extension.test.js +367 -0
- package/pi-extension/test/helpers.test.js +179 -0
- package/plugin.yaml +21 -12
- package/rules/antislop.mdc +18 -0
- package/scripts/build-openclaw-skills.js +60 -0
- package/scripts/check-repo.mjs +389 -0
- package/scripts/check-rule-copies.js +74 -0
- package/scripts/check-versions.js +77 -0
- package/scripts/cursor-hooks.js +145 -0
- package/scripts/generate-chillslop.mjs +42 -0
- package/scripts/generate-examples.mjs +149 -0
- package/scripts/generate-plugin-doors.mjs +108 -0
- package/scripts/publish-openclaw-skills.js +75 -0
- package/scripts/skill-index.mjs +81 -0
- package/scripts/sync-ponytail.mjs +65 -0
- package/scripts/uninstall.js +92 -0
- package/skills/antislop/SKILL.md +700 -0
- package/skills/antislop/VERSION +1 -0
- package/skills/antislop-code/SKILL.md +128 -0
- package/skills/antislop-copywriting/SKILL.md +407 -0
- package/skills/antislop-human/LICENSE-web-quality-skills +21 -0
- package/skills/antislop-human/SKILL.md +204 -0
- package/skills/antislop-human/contrast-check.py +168 -0
- package/skills/antislop-human/contrast-mcp.py +121 -0
- package/skills/antislop-human/references/A11Y-PATTERNS.md +249 -0
- package/skills/antislop-human/references/WCAG.md +127 -0
- package/skills/antislop-layoutmobile/SKILL.md +170 -0
- package/skills/antislop-ui/LICENSE-web-quality-skills +21 -0
- package/skills/antislop-ui/SKILL.md +344 -0
- package/skills/antislop-ui/references/CORE-WEB-VITALS.md +181 -0
- package/skills/antislop-ui/references/WEB-BASELINE.md +28 -0
- package/skills/plugin.json +5 -0
- package/skills/ponytail/SKILL.md +52 -0
- package/skills/ponytail-audit/SKILL.md +103 -0
- package/skills/ponytail-debt/SKILL.md +44 -0
- package/skills/ponytail-gain/SKILL.md +52 -0
- package/skills/ponytail-help/SKILL.md +76 -0
- package/skills/ponytail-review/SKILL.md +101 -0
- package/src/package.json +3 -0
- package/tests/activation-map.test.js +63 -0
- package/tests/band-import.test.js +111 -0
- package/tests/band.test.tsx +520 -0
- package/tests/behavior.test.js +80 -0
- package/tests/commands.test.js +94 -0
- package/tests/copilot-plugin.test.js +35 -0
- package/tests/correctness.test.js +245 -0
- package/tests/cursor-hooks.test.js +343 -0
- package/tests/gemini-extension.test.js +91 -0
- package/tests/grok-plugin.test.js +39 -0
- package/tests/hermes-plugin.test.js +403 -0
- package/tests/hooks-windows.test.js +205 -0
- package/tests/hooks.test.js +1005 -0
- package/tests/kimi-plugin.test.js +25 -0
- package/tests/loc.test.js +18 -0
- package/tests/map.test.js +30 -0
- package/tests/openclaw-skills.test.js +26 -0
- package/tests/opencode-plugin.test.js +340 -0
- package/tests/package-scripts.test.js +35 -0
- package/tests/package.test.js +23 -0
- package/tests/qoder-plugin.test.js +84 -0
- package/tests/uninstall.test.js +202 -0
- /package/bin/{chill-agent.js → chill-agent.mjs} +0 -0
- /package/tests/{manifests.test.js → manifests.test.mjs} +0 -0
- /package/tests/{rules.test.js → rules.test.mjs} +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: caveman
|
|
3
|
+
description: >
|
|
4
|
+
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
|
|
5
|
+
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra,
|
|
6
|
+
wenyan-lite, wenyan-full, wenyan-ultra.
|
|
7
|
+
Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens",
|
|
8
|
+
"be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Respond terse like smart caveman. All technical substance stay. Only fluff die.
|
|
12
|
+
|
|
13
|
+
## Persistence
|
|
14
|
+
|
|
15
|
+
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".
|
|
16
|
+
|
|
17
|
+
Default: **full**. Switch: `/caveman lite|full|ultra`.
|
|
18
|
+
|
|
19
|
+
## Rules
|
|
20
|
+
|
|
21
|
+
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
|
|
22
|
+
|
|
23
|
+
Pattern: `[thing] [action] [reason]. [next step].`
|
|
24
|
+
|
|
25
|
+
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
|
|
26
|
+
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
|
|
27
|
+
|
|
28
|
+
## Intensity
|
|
29
|
+
|
|
30
|
+
| Level | What change |
|
|
31
|
+
|-------|------------|
|
|
32
|
+
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
|
|
33
|
+
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman |
|
|
34
|
+
| **ultra** | Abbreviate (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough |
|
|
35
|
+
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
|
|
36
|
+
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
|
|
37
|
+
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
|
|
38
|
+
|
|
39
|
+
Example — "Why React component re-render?"
|
|
40
|
+
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
|
|
41
|
+
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
|
|
42
|
+
- ultra: "Inline obj prop → new ref → re-render. `useMemo`."
|
|
43
|
+
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
|
|
44
|
+
- wenyan-full: "物出新參照,致重繪。useMemo .Wrap之。"
|
|
45
|
+
- wenyan-ultra: "新參照→重繪。useMemo Wrap。"
|
|
46
|
+
|
|
47
|
+
Example — "Explain database connection pooling."
|
|
48
|
+
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
|
|
49
|
+
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
|
|
50
|
+
- ultra: "Pool = reuse DB conn. Skip handshake → fast under load."
|
|
51
|
+
- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。"
|
|
52
|
+
- wenyan-ultra: "池reuse conn。skip handshake → fast。"
|
|
53
|
+
|
|
54
|
+
## Auto-Clarity
|
|
55
|
+
|
|
56
|
+
Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user asks to clarify or repeats question. Resume caveman after clear part done.
|
|
57
|
+
|
|
58
|
+
Example — destructive op:
|
|
59
|
+
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
|
|
60
|
+
> ```sql
|
|
61
|
+
> DROP TABLE users;
|
|
62
|
+
> ```
|
|
63
|
+
> Caveman resume. Verify backup exist first.
|
|
64
|
+
|
|
65
|
+
## Boundaries
|
|
66
|
+
|
|
67
|
+
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Caveman arm: caveman SKILL.md (full) as the system prompt.
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const system = fs.readFileSync(path.join(__dirname, 'caveman-SKILL.md'), 'utf8');
|
|
5
|
+
module.exports = ({ vars }) => [
|
|
6
|
+
{ role: 'system', content: system },
|
|
7
|
+
{ role: 'user', content: vars.task },
|
|
8
|
+
];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Ponytail arm: the repo's own SKILL.md (full) as the system prompt. Single source of truth.
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const system = fs.readFileSync(path.join(__dirname, '..', '..', 'skills', 'ponytail', 'SKILL.md'), 'utf8');
|
|
5
|
+
module.exports = ({ vars }) => [
|
|
6
|
+
{ role: 'system', content: system },
|
|
7
|
+
{ role: 'user', content: vars.task },
|
|
8
|
+
];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Behavior gate: does the ponytail ruleset actually PRODUCE its refined
|
|
2
|
+
// behaviors, not just carry the text? One check per probe (vars.probe), each
|
|
3
|
+
// targeting a rule that a field review (rcstack, phases 0-8) showed mattered:
|
|
4
|
+
// hardware - "hardware is never the spec ideal, leave the calibration knob"
|
|
5
|
+
// explanation - "explanation the user explicitly asked for is not debt"
|
|
6
|
+
// onecheck - "lazy code without its check is unfinished"
|
|
7
|
+
//
|
|
8
|
+
// Heuristic graders, same spirit as loc.js / correctness.js. The graders
|
|
9
|
+
// themselves are proven by tests/behavior.test.js (RED/GREEN, no API key).
|
|
10
|
+
//
|
|
11
|
+
// Metric: `behavior` (1 = behavior present, 0 = absent).
|
|
12
|
+
|
|
13
|
+
function proseOf(text) {
|
|
14
|
+
return String(text || '').replace(/```[\s\S]*?```/g, ' ').replace(/\s+/g, ' ').trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const CHECKS = {
|
|
18
|
+
// Treats the device as non-ideal: leaves a tunable knob or flags per-unit drift.
|
|
19
|
+
// A passing mention of "calibration" is not enough; it must be actionable.
|
|
20
|
+
hardware(output) {
|
|
21
|
+
const t = String(output || '');
|
|
22
|
+
const drift = /\bdrift|per[- ]unit|per[- ]part|part[- ]to[- ]part|measure your own|\btare\b|\btrim\b|\bknob|\btuning\b|reads off|known (temp|reference|value)|reference (thermometer|sensor|temp)|calibration (offset|constant|param|knob)/i.test(t);
|
|
23
|
+
return drift
|
|
24
|
+
? { pass: true, reason: 'Leaves a calibration knob / flags per-unit drift.' }
|
|
25
|
+
: { pass: false, reason: 'Treats the hardware as ideal; no calibration knob.' };
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
// Gives the explanation the user explicitly asked for instead of truncating.
|
|
29
|
+
explanation(output) {
|
|
30
|
+
const p = proseOf(output);
|
|
31
|
+
const words = p ? p.split(' ').length : 0;
|
|
32
|
+
const structured = /(\d+[.)]\s|[-*]\s)/.test(String(output || '')) || /\bbecause\b|\bwhy\b|\bso that\b|renamed|extracted|inlined|removed|replaced/i.test(p);
|
|
33
|
+
return words >= 45 && structured
|
|
34
|
+
? { pass: true, reason: `Gave the requested write-up (${words} words of prose).` }
|
|
35
|
+
: { pass: false, reason: `Truncated the requested explanation (${words} words of prose).` };
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Leaves ONE runnable check behind for non-trivial logic.
|
|
39
|
+
onecheck(output) {
|
|
40
|
+
const t = String(output || '');
|
|
41
|
+
const hasCheck = /\bassert\b|def\s+test_|if\s+__name__|unittest|pytest|console\.assert|\bexpect\(|\bdescribe\(|\bit\(/.test(t);
|
|
42
|
+
return hasCheck
|
|
43
|
+
? { pass: true, reason: 'Left a runnable check (assert/test/demo).' }
|
|
44
|
+
: { pass: false, reason: 'No runnable check left behind.' };
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
module.exports = (output, context) => {
|
|
49
|
+
const probe = context && context.vars && context.vars.probe;
|
|
50
|
+
const check = CHECKS[probe];
|
|
51
|
+
if (!check) return { pass: true, score: 1, reason: `Unknown probe '${probe}', skipped` };
|
|
52
|
+
const r = check(output);
|
|
53
|
+
return { pass: r.pass, score: r.pass ? 1 : 0, reason: r.reason };
|
|
54
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Ponytail behavior gates: does the ruleset actually produce its refined
|
|
2
|
+
# behaviors (not just carry the text)? Probes the three rules a full-project
|
|
3
|
+
# field review (rcstack, phases 0-8) showed mattered.
|
|
4
|
+
#
|
|
5
|
+
# Run: npx promptfoo@latest eval -c benchmarks/behavior.yaml --repeat 10
|
|
6
|
+
# View: npx promptfoo@latest view
|
|
7
|
+
#
|
|
8
|
+
# Needs ANTHROPIC_API_KEY (see benchmarks/README.md). The grader (behavior.js)
|
|
9
|
+
# is proven separately by tests/behavior.test.js, which needs no API key.
|
|
10
|
+
#
|
|
11
|
+
# baseline is included as the control: the no-skill arm should mostly FAIL these
|
|
12
|
+
# gates, the ponytail arm should pass them. That delta is the point.
|
|
13
|
+
description: "Ponytail behavior gates: hardware calibration, requested explanation, one runnable check."
|
|
14
|
+
|
|
15
|
+
providers:
|
|
16
|
+
- id: anthropic:messages:claude-opus-4-8
|
|
17
|
+
config: { max_tokens: 8192, temperature: 1 }
|
|
18
|
+
|
|
19
|
+
prompts:
|
|
20
|
+
- id: file://arms/baseline.js
|
|
21
|
+
label: baseline (no skill)
|
|
22
|
+
- id: file://arms/ponytail.js
|
|
23
|
+
label: ponytail
|
|
24
|
+
|
|
25
|
+
defaultTest:
|
|
26
|
+
assert:
|
|
27
|
+
- type: javascript
|
|
28
|
+
value: file://behavior.js
|
|
29
|
+
metric: behavior
|
|
30
|
+
|
|
31
|
+
tests:
|
|
32
|
+
- vars:
|
|
33
|
+
probe: hardware
|
|
34
|
+
task: "Write a Python function that reads the temperature in Celsius from a thermistor wired to a Raspberry Pi ADC (MCP3008, channel 0)."
|
|
35
|
+
- vars:
|
|
36
|
+
probe: explanation
|
|
37
|
+
task: "Refactor this for readability and give me a detailed, step-by-step write-up of every change you made and why.\n\ndef p(d):\n r = []\n for x in d:\n if x.get('a') and x['a'] > 0:\n r.append(x['a'] * 2)\n return r"
|
|
38
|
+
- vars:
|
|
39
|
+
probe: onecheck
|
|
40
|
+
task: "Write a Python function that parses a duration string like '1h30m45s' into a total number of seconds."
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// Functional correctness assertion: runs generated code against lightweight test
|
|
2
|
+
// cases per task. Proves "less code" is not "broken code". Spawns python/node
|
|
3
|
+
// with the extracted code + appended assertions; returns pass/fail + score.
|
|
4
|
+
//
|
|
5
|
+
// Metric: `correct` (1 = all checks pass, 0 = at least one fails).
|
|
6
|
+
// Unlike loc.js (measurement-only), this one is a gate — a wrong answer is a
|
|
7
|
+
// wrong answer regardless of how few lines produced it.
|
|
8
|
+
//
|
|
9
|
+
// Note: email, debounce, and CSV checks execute the generated code against
|
|
10
|
+
// real inputs. Countdown (React) and rate-limit (FastAPI) checks are
|
|
11
|
+
// structural-only: they verify plausible code shape rather than runtime
|
|
12
|
+
// behaviour, because a React bundler or a live FastAPI server is needed to
|
|
13
|
+
// execute those properly.
|
|
14
|
+
|
|
15
|
+
const { execSync } = require('child_process');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
function correctnessTimeoutMs() {
|
|
21
|
+
const value = Number.parseInt(process.env.PONYTAIL_CORRECTNESS_TIMEOUT_MS || '', 10);
|
|
22
|
+
return Number.isFinite(value) && value > 0 ? value : 30_000;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Extract fenced code blocks, tagged by language.
|
|
26
|
+
function extractBlocks(text) {
|
|
27
|
+
text = String(text || '');
|
|
28
|
+
const matches = [...text.matchAll(/```(\w*)\r?\n([\s\S]*?)```/g)];
|
|
29
|
+
// ponytail: terse models often answer with bare, unfenced code. Treat the whole
|
|
30
|
+
// response as one block so the gate scores the code instead of reporting "no block".
|
|
31
|
+
if (matches.length === 0 && text.trim()) return [{ lang: '', code: text }];
|
|
32
|
+
return matches.map((m) => ({ lang: (m[1] || '').toLowerCase(), code: m[2] }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Identify which task we're evaluating from vars.task.
|
|
36
|
+
function identifyTask(task) {
|
|
37
|
+
const t = task.toLowerCase();
|
|
38
|
+
if (t.includes('email') && t.includes('valid')) return 'email';
|
|
39
|
+
if (t.includes('debounce')) return 'debounce';
|
|
40
|
+
if (t.includes('csv') && t.includes('sum')) return 'csv';
|
|
41
|
+
if (t.includes('countdown') && t.includes('react')) return 'countdown';
|
|
42
|
+
if (t.includes('rate limit') || t.includes('rate-limit')) return 'ratelimit';
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Run a command, return { ok, stderr }. Harnesses that print FAIL to stdout
|
|
47
|
+
// still get their reason through (#919).
|
|
48
|
+
function exec(cmd, opts = {}) {
|
|
49
|
+
try {
|
|
50
|
+
execSync(cmd, { timeout: correctnessTimeoutMs(), encoding: 'utf8', stdio: 'pipe', ...opts });
|
|
51
|
+
return { ok: true, stderr: '' };
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return { ok: false, stderr: (e.stderr || e.stdout || e.message || '').slice(0, 500) };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ponytail: probe once at load; macOS and many Linux images ship python3 only.
|
|
58
|
+
// Prefer one with pandas (the csv task needs it): on Windows python3 can be a
|
|
59
|
+
// bundled Python without it while python has it (#919).
|
|
60
|
+
let pythonCmd;
|
|
61
|
+
function python() {
|
|
62
|
+
if (pythonCmd) return pythonCmd;
|
|
63
|
+
const candidates = ['python3', 'python'];
|
|
64
|
+
pythonCmd = candidates.find((cmd) => exec(`${cmd} -c "import pandas"`).ok)
|
|
65
|
+
|| candidates.find((cmd) => exec(`${cmd} -c "import sys"`).ok)
|
|
66
|
+
|| 'python3';
|
|
67
|
+
return pythonCmd;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Write content to a temp file, return the path.
|
|
71
|
+
function tmpFile(ext, content) {
|
|
72
|
+
const p = path.join(os.tmpdir(), `ponytail-bench-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
|
|
73
|
+
fs.writeFileSync(p, content);
|
|
74
|
+
return p;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// --- Per-task test harnesses ---
|
|
78
|
+
|
|
79
|
+
const CHECKS = {
|
|
80
|
+
email(blocks) {
|
|
81
|
+
const code = blocks.find((b) => b.lang === 'python' || b.lang === 'py' || (!b.lang && b.code.includes('def ')));
|
|
82
|
+
if (!code) return { pass: false, reason: 'No Python code block found' };
|
|
83
|
+
|
|
84
|
+
// Append assertions that call the generated function by common names.
|
|
85
|
+
const harness = `
|
|
86
|
+
${code.code}
|
|
87
|
+
|
|
88
|
+
# Find the validator function
|
|
89
|
+
import sys
|
|
90
|
+
fn = None
|
|
91
|
+
for name in ['validate_email', 'is_valid_email', 'email_validator', 'is_valid', 'validate']:
|
|
92
|
+
if name in dir() and callable(eval(name)):
|
|
93
|
+
fn = eval(name)
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
if fn is None:
|
|
97
|
+
# Try any function that takes one arg
|
|
98
|
+
import inspect
|
|
99
|
+
for name, obj in list(globals().items()):
|
|
100
|
+
if callable(obj) and not name.startswith('_'):
|
|
101
|
+
try:
|
|
102
|
+
sig = inspect.signature(obj)
|
|
103
|
+
if len(sig.parameters) == 1:
|
|
104
|
+
fn = obj
|
|
105
|
+
break
|
|
106
|
+
except (ValueError, TypeError):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
if fn is None:
|
|
110
|
+
print("FAIL: no validator function found")
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
|
|
113
|
+
# Test cases
|
|
114
|
+
failures = []
|
|
115
|
+
if not fn("user@example.com"):
|
|
116
|
+
failures.append("rejected valid: user@example.com")
|
|
117
|
+
if not fn("a@b.co"):
|
|
118
|
+
failures.append("rejected valid: a@b.co")
|
|
119
|
+
if fn("no-at-sign"):
|
|
120
|
+
failures.append("accepted invalid: no-at-sign")
|
|
121
|
+
if fn(""):
|
|
122
|
+
failures.append("accepted invalid: empty string")
|
|
123
|
+
if fn("@missing-local.com"):
|
|
124
|
+
failures.append("accepted invalid: @missing-local.com")
|
|
125
|
+
|
|
126
|
+
if failures:
|
|
127
|
+
print("FAIL: " + "; ".join(failures))
|
|
128
|
+
sys.exit(1)
|
|
129
|
+
print("PASS")
|
|
130
|
+
`;
|
|
131
|
+
const f = tmpFile('.py', harness);
|
|
132
|
+
const result = exec(`${python()} "${f}"`);
|
|
133
|
+
fs.unlinkSync(f);
|
|
134
|
+
if (result.ok) return { pass: true, reason: 'Email validator passes all checks' };
|
|
135
|
+
return { pass: false, reason: result.stderr || 'Email validator failed' };
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
debounce(blocks) {
|
|
139
|
+
const code = blocks.find((b) => b.lang === 'javascript' || b.lang === 'js' || (!b.lang && (b.code.includes('function') || b.code.includes('=>'))));
|
|
140
|
+
if (!code) return { pass: false, reason: 'No JavaScript code block found' };
|
|
141
|
+
|
|
142
|
+
const harness = `
|
|
143
|
+
${code.code}
|
|
144
|
+
|
|
145
|
+
// Find the debounce function
|
|
146
|
+
const fn = typeof debounce === 'function' ? debounce
|
|
147
|
+
: typeof module !== 'undefined' && typeof module.exports === 'function' ? module.exports
|
|
148
|
+
: null;
|
|
149
|
+
|
|
150
|
+
if (!fn) {
|
|
151
|
+
console.error("FAIL: no debounce function found");
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Test: debounced function should not fire immediately
|
|
156
|
+
let callCount = 0;
|
|
157
|
+
const debounced = fn(() => { callCount++; }, 50);
|
|
158
|
+
debounced();
|
|
159
|
+
debounced();
|
|
160
|
+
debounced();
|
|
161
|
+
|
|
162
|
+
if (callCount > 0) {
|
|
163
|
+
console.error("FAIL: debounce fired immediately (should wait)");
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Test: should fire after the delay
|
|
168
|
+
setTimeout(() => {
|
|
169
|
+
if (callCount !== 1) {
|
|
170
|
+
console.error("FAIL: expected 1 call after delay, got " + callCount);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
console.log("PASS");
|
|
174
|
+
}, 120);
|
|
175
|
+
`;
|
|
176
|
+
const f = tmpFile('.mjs', harness);
|
|
177
|
+
const result = exec(`node "${f}"`);
|
|
178
|
+
fs.unlinkSync(f);
|
|
179
|
+
if (result.ok) return { pass: true, reason: 'Debounce passes all checks' };
|
|
180
|
+
return { pass: false, reason: result.stderr || 'Debounce failed' };
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
csv(blocks) {
|
|
184
|
+
const code = blocks.find((b) => b.lang === 'python' || b.lang === 'py' || (!b.lang && b.code.includes('csv') && b.code.includes('sum')));
|
|
185
|
+
if (!code) return { pass: false, reason: 'No Python code block found' };
|
|
186
|
+
|
|
187
|
+
// Create a test CSV and wrap the generated code so it reads it.
|
|
188
|
+
const csvContent = 'name,amount\nAlice,100.5\nBob,200.0\nCharlie,50.5\n';
|
|
189
|
+
const csvPath = tmpFile('.csv', csvContent).replace(/\\/g, '/');
|
|
190
|
+
|
|
191
|
+
// The generated code likely reads 'sales.csv'; patch the filename.
|
|
192
|
+
let patched = code.code.replace(/['"]sales\.csv['"]/g, `'${csvPath}'`);
|
|
193
|
+
// Also try open() calls
|
|
194
|
+
patched = patched.replace(/open\(\s*['"]sales\.csv['"]/g, `open('${csvPath}'`);
|
|
195
|
+
|
|
196
|
+
const harness = `
|
|
197
|
+
import sys, os
|
|
198
|
+
os.chdir(r"${path.dirname(csvPath)}")
|
|
199
|
+
|
|
200
|
+
# Capture print output
|
|
201
|
+
import io
|
|
202
|
+
_stdout = sys.stdout
|
|
203
|
+
sys.stdout = io.StringIO()
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
${patched.split('\n').map((l) => ' ' + l).join('\n')}
|
|
207
|
+
except Exception:
|
|
208
|
+
sys.stdout = _stdout
|
|
209
|
+
raise
|
|
210
|
+
|
|
211
|
+
output = sys.stdout.getvalue()
|
|
212
|
+
sys.stdout = _stdout
|
|
213
|
+
|
|
214
|
+
# Check output contains the number 351 (100.5 + 200.0 + 50.5)
|
|
215
|
+
# Match as a standalone number (not as substring of e.g. 13510)
|
|
216
|
+
import re
|
|
217
|
+
if re.search(r'(?<![\\d])351(?:\\.0)?(?![\\d])', output):
|
|
218
|
+
print("PASS")
|
|
219
|
+
else:
|
|
220
|
+
# Try running it differently: maybe it defines a function
|
|
221
|
+
print("FAIL: output was: " + repr(output[:200]))
|
|
222
|
+
sys.exit(1)
|
|
223
|
+
`;
|
|
224
|
+
const f = tmpFile('.py', harness);
|
|
225
|
+
const result = exec(`${python()} "${f}"`);
|
|
226
|
+
try { fs.unlinkSync(f); } catch (e) {}
|
|
227
|
+
try { fs.unlinkSync(csvPath); } catch (e) {}
|
|
228
|
+
if (result.ok) return { pass: true, reason: 'CSV sum produces correct result (351)' };
|
|
229
|
+
return { pass: false, reason: result.stderr || 'CSV sum failed' };
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
countdown(blocks) {
|
|
233
|
+
// React components can't run in bare Node without a bundler. Structural check:
|
|
234
|
+
// the code must contain timer/countdown logic (useState/useEffect/setInterval/setTimeout).
|
|
235
|
+
const code = blocks.find((b) => b.code.includes('ount') || b.code.includes('timer') || b.code.includes('Timer'));
|
|
236
|
+
if (!code) return { pass: false, reason: 'No countdown component found' };
|
|
237
|
+
|
|
238
|
+
const src = code.code;
|
|
239
|
+
const hasState = /useState|useReducer|this\.state/.test(src);
|
|
240
|
+
const hasEffect = /useEffect|componentDidMount|setInterval|setTimeout/.test(src);
|
|
241
|
+
const hasDecrement = /- 1|-= 1|prev - 1|count - 1|seconds - 1|time - 1/.test(src);
|
|
242
|
+
|
|
243
|
+
const failures = [];
|
|
244
|
+
if (!hasState) failures.push('no state management (useState/useReducer)');
|
|
245
|
+
if (!hasEffect) failures.push('no timer setup (useEffect/setInterval/setTimeout)');
|
|
246
|
+
if (!hasDecrement) failures.push('no countdown decrement logic');
|
|
247
|
+
|
|
248
|
+
if (failures.length === 0) return { pass: true, reason: 'Countdown has required structure' };
|
|
249
|
+
return { pass: false, reason: 'Missing: ' + failures.join(', ') };
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
ratelimit(blocks) {
|
|
253
|
+
const code = blocks.find((b) => b.lang === 'python' || b.lang === 'py' || (!b.lang && (b.code.includes('rate') || b.code.includes('limit'))));
|
|
254
|
+
if (!code) return { pass: false, reason: 'No Python code block found' };
|
|
255
|
+
|
|
256
|
+
// Structural check for rate limiting: must have some form of counter/time tracking.
|
|
257
|
+
const src = code.code;
|
|
258
|
+
const hasTimeTracking = /time\.|datetime|asyncio/.test(src);
|
|
259
|
+
const hasLimitLogic = /limit|max_requests|rate|429|Too Many|HTTPException|RateLimiter/.test(src);
|
|
260
|
+
const hasFastAPI = /fastapi|FastAPI|app\s*=|@app\./.test(src);
|
|
261
|
+
|
|
262
|
+
const failures = [];
|
|
263
|
+
if (!hasLimitLogic) failures.push('no rate limit logic');
|
|
264
|
+
if (!hasFastAPI) failures.push('no FastAPI usage');
|
|
265
|
+
|
|
266
|
+
if (failures.length === 0) return { pass: true, reason: 'Rate limiter has required structure' };
|
|
267
|
+
return { pass: false, reason: 'Missing: ' + failures.join(', ') };
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// --- Main assertion entry point ---
|
|
272
|
+
|
|
273
|
+
module.exports = (output, context) => {
|
|
274
|
+
const task = identifyTask(context.vars.task || '');
|
|
275
|
+
if (!task) {
|
|
276
|
+
return { pass: true, score: 1, reason: 'Unknown task, skipped correctness check' };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const blocks = extractBlocks(String(output || ''));
|
|
280
|
+
if (blocks.length === 0) {
|
|
281
|
+
return { pass: false, score: 0, reason: 'No code blocks in output' };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const check = CHECKS[task];
|
|
285
|
+
const result = check(blocks);
|
|
286
|
+
return {
|
|
287
|
+
pass: result.pass,
|
|
288
|
+
score: result.pass ? 1 : 0,
|
|
289
|
+
reason: result.reason,
|
|
290
|
+
};
|
|
291
|
+
};
|
package/benchmarks/loc.js
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
// Deterministic code-size metric: non-blank, non-comment lines of code. Counts
|
|
2
|
+
// fenced blocks, or the whole response when the model emitted bare code unfenced.
|
|
3
|
+
// Recorded as the `code_loc` metric per arm (always passes; it is a measurement, not a gate).
|
|
4
|
+
function loc(output) {
|
|
5
|
+
const text = String(output || '');
|
|
6
|
+
const blocks = [...text.matchAll(/```[a-zA-Z0-9_+-]*\r?\n([\s\S]*?)```/g)].map((m) => m[1]);
|
|
7
|
+
// Drop /* ... */ block comments before counting; the line filter below only
|
|
8
|
+
// caught `*`-aligned JSDoc, so plain block comments were miscounted as code.
|
|
9
|
+
const code = (blocks.length ? blocks.join('\n') : text).replace(/\/\*[\s\S]*?\*\//g, '');
|
|
10
|
+
const count = code
|
|
11
|
+
.split('\n')
|
|
12
|
+
.map((l) => l.trim())
|
|
13
|
+
.filter((l) => l && !l.startsWith('//') && !l.startsWith('#') && l !== '*/' && !l.startsWith('/*') && !l.startsWith('*')).length;
|
|
14
|
+
return { pass: true, score: count, reason: count + ' code LOC' };
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const raw = fs.readFileSync(file, 'utf8');
|
|
17
|
-
console.log(`File: ${file}`);
|
|
18
|
-
console.log(`Raw Lines: ${raw.split('\n').length}`);
|
|
19
|
-
console.log(`Net Logic LOC: ${countLoc(raw)}`);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
17
|
+
loc.loc = loc;
|
|
18
|
+
loc.countLoc = loc;
|
|
19
|
+
module.exports = loc;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Ponytail vs baseline, latest Gemini: gemini-3.5-flash (mini) + gemini-3.1-pro-preview (top).
|
|
2
|
+
# npx promptfoo@latest eval -c benchmarks/promptfooconfig.gemini.yaml --env-file .env --repeat 30
|
|
3
|
+
# Needs GOOGLE_API_KEY in .env (AI Studio).
|
|
4
|
+
description: "Ponytail vs baseline, latest Gemini (3.5-flash, 3.1-pro). LOC + correctness, cost telemetry."
|
|
5
|
+
|
|
6
|
+
providers:
|
|
7
|
+
- id: google:gemini-3.5-flash
|
|
8
|
+
config: { temperature: 1, maxOutputTokens: 8192 }
|
|
9
|
+
- id: google:gemini-3.1-pro-preview
|
|
10
|
+
config: { maxOutputTokens: 8192 }
|
|
11
|
+
|
|
12
|
+
prompts:
|
|
13
|
+
- id: file://arms/baseline.js
|
|
14
|
+
label: baseline (no skill)
|
|
15
|
+
- id: file://arms/ponytail.js
|
|
16
|
+
label: ponytail
|
|
17
|
+
|
|
18
|
+
defaultTest:
|
|
19
|
+
assert:
|
|
20
|
+
- type: javascript
|
|
21
|
+
value: file://loc.js
|
|
22
|
+
metric: code_loc
|
|
23
|
+
- type: javascript
|
|
24
|
+
value: file://correctness.js
|
|
25
|
+
metric: correct
|
|
26
|
+
|
|
27
|
+
tests:
|
|
28
|
+
- vars: { task: "Write me a Python function that validates email addresses." }
|
|
29
|
+
- vars: { task: "Write a reusable debounce function in vanilla JavaScript: debounce(fn, delay) returns a debounced version of fn that delays calling it until delay ms after the last call." }
|
|
30
|
+
- vars: { task: "Write Python code that reads sales.csv and sums the 'amount' column." }
|
|
31
|
+
- vars: { task: "Build me a countdown timer component in React that counts down from a given number of seconds." }
|
|
32
|
+
- vars: { task: "Add rate limiting to my FastAPI endpoint so users can't spam it." }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Ponytail vs baseline, newest OpenAI: gpt-5.5 (top) + gpt-4.1-mini, gpt-5.4-mini.
|
|
2
|
+
# npx promptfoo@latest eval -c benchmarks/promptfooconfig.gpt-newest.yaml --env-file .env --repeat 30
|
|
3
|
+
description: "Ponytail vs baseline, newest OpenAI (gpt-5.5 + minis). LOC + correctness, cost telemetry."
|
|
4
|
+
|
|
5
|
+
providers:
|
|
6
|
+
- id: openai:gpt-5.5
|
|
7
|
+
config: { max_completion_tokens: 8192 }
|
|
8
|
+
- id: openai:gpt-4.1-mini
|
|
9
|
+
config: { max_tokens: 8192, temperature: 1 }
|
|
10
|
+
- id: openai:gpt-5.4-mini
|
|
11
|
+
config: { max_completion_tokens: 8192 }
|
|
12
|
+
|
|
13
|
+
prompts:
|
|
14
|
+
- id: file://arms/baseline.js
|
|
15
|
+
label: baseline (no skill)
|
|
16
|
+
- id: file://arms/ponytail.js
|
|
17
|
+
label: ponytail
|
|
18
|
+
|
|
19
|
+
defaultTest:
|
|
20
|
+
assert:
|
|
21
|
+
- type: javascript
|
|
22
|
+
value: file://loc.js
|
|
23
|
+
metric: code_loc
|
|
24
|
+
- type: javascript
|
|
25
|
+
value: file://correctness.js
|
|
26
|
+
metric: correct
|
|
27
|
+
|
|
28
|
+
tests:
|
|
29
|
+
- vars: { task: "Write me a Python function that validates email addresses." }
|
|
30
|
+
- vars: { task: "Write a reusable debounce function in vanilla JavaScript: debounce(fn, delay) returns a debounced version of fn that delays calling it until delay ms after the last call." }
|
|
31
|
+
- vars: { task: "Write Python code that reads sales.csv and sums the 'amount' column." }
|
|
32
|
+
- vars: { task: "Build me a countdown timer component in React that counts down from a given number of seconds." }
|
|
33
|
+
- vars: { task: "Add rate limiting to my FastAPI endpoint so users can't spam it." }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Ponytail benchmark: code size + cost across three arms, same model, same tasks.
|
|
2
|
+
#
|
|
3
|
+
# Run: npx promptfoo@latest eval -c benchmarks/promptfooconfig.yaml
|
|
4
|
+
# View: npx promptfoo@latest view
|
|
5
|
+
# Share: npx promptfoo@latest share (publishes a hosted report URL)
|
|
6
|
+
#
|
|
7
|
+
# Needs ANTHROPIC_API_KEY in the environment or a .env file (see benchmarks/README.md).
|
|
8
|
+
# Caveman arm uses JuliusBrussee/caveman SKILL.md (MIT), vendored at arms/caveman-SKILL.md.
|
|
9
|
+
description: "Ponytail vs caveman vs no-skill: same model, same tasks. Measures code LOC (deterministic) and tokens/cost (API telemetry)."
|
|
10
|
+
|
|
11
|
+
providers:
|
|
12
|
+
- id: anthropic:messages:claude-haiku-4-5-20251001
|
|
13
|
+
config: { max_tokens: 8192, temperature: 1 }
|
|
14
|
+
- id: anthropic:messages:claude-sonnet-4-6
|
|
15
|
+
config: { max_tokens: 8192, temperature: 1 }
|
|
16
|
+
- id: anthropic:messages:claude-opus-4-8
|
|
17
|
+
config: { max_tokens: 8192, temperature: 1 }
|
|
18
|
+
|
|
19
|
+
prompts:
|
|
20
|
+
- id: file://arms/baseline.js
|
|
21
|
+
label: baseline (no skill)
|
|
22
|
+
- id: file://arms/caveman.js
|
|
23
|
+
label: caveman
|
|
24
|
+
- id: file://arms/ponytail.js
|
|
25
|
+
label: ponytail
|
|
26
|
+
|
|
27
|
+
defaultTest:
|
|
28
|
+
assert:
|
|
29
|
+
- type: javascript
|
|
30
|
+
value: file://loc.js
|
|
31
|
+
metric: code_loc
|
|
32
|
+
- type: javascript
|
|
33
|
+
value: file://correctness.js
|
|
34
|
+
metric: correct
|
|
35
|
+
|
|
36
|
+
tests:
|
|
37
|
+
- vars: { task: "Write me a Python function that validates email addresses." }
|
|
38
|
+
- vars: { task: "Write a reusable debounce function in vanilla JavaScript: debounce(fn, delay) returns a debounced version of fn that delays calling it until delay ms after the last call." }
|
|
39
|
+
- vars: { task: "Write Python code that reads sales.csv and sums the 'amount' column." }
|
|
40
|
+
- vars: { task: "Build me a countdown timer component in React that counts down from a given number of seconds." }
|
|
41
|
+
- vars: { task: "Add rate limiting to my FastAPI endpoint so users can't spam it." }
|