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.
Files changed (240) hide show
  1. package/.agents/rules/ponytail.md +30 -0
  2. package/.claude-plugin/contrast-mcp-launcher.mjs +31 -0
  3. package/.claude-plugin/marketplace.json +17 -0
  4. package/.claude-plugin/plugin.json +8 -3
  5. package/.cline-plugin/plugin.js +6 -0
  6. package/.clinerules/ponytail.md +30 -0
  7. package/.codex-plugin/plugin.json +4 -3
  8. package/.cursor/rules/ponytail.mdc +36 -0
  9. package/.cursor-plugin/marketplace.json +16 -0
  10. package/.cursor-plugin/plugin.json +2 -2
  11. package/.devin-plugin/plugin.json +2 -2
  12. package/.grok-plugin/marketplace.json +16 -0
  13. package/.kimi-plugin/plugin.json +11 -29
  14. package/.kiro/steering/ponytail.md +35 -0
  15. package/.omp-plugin/marketplace.json +15 -0
  16. package/.openclaw/skills/ponytail/SKILL.md +47 -0
  17. package/.openclaw/skills/ponytail-audit/SKILL.md +100 -0
  18. package/.openclaw/skills/ponytail-debt/SKILL.md +43 -0
  19. package/.openclaw/skills/ponytail-gain/SKILL.md +51 -0
  20. package/.openclaw/skills/ponytail-help/SKILL.md +76 -0
  21. package/.openclaw/skills/ponytail-review/SKILL.md +98 -0
  22. package/.opencode/command/ponytail-audit.md +5 -0
  23. package/.opencode/command/ponytail-debt.md +5 -0
  24. package/.opencode/command/ponytail-gain.md +5 -0
  25. package/.opencode/command/ponytail-help.md +5 -0
  26. package/.opencode/command/ponytail-review.md +5 -0
  27. package/.opencode/command/ponytail.md +5 -0
  28. package/.opencode/plugins/index.js +4 -0
  29. package/.opencode/plugins/ponytail-frontmatter.cjs +63 -0
  30. package/.opencode/plugins/ponytail.mjs +206 -0
  31. package/.qoder-plugin/plugin.json +12 -26
  32. package/.windsurf/rules/ponytail.md +30 -0
  33. package/AGENTS.md +32 -0
  34. package/__init__.py +228 -0
  35. package/assets/antislop-banner.png +0 -0
  36. package/assets/band/band.png +0 -0
  37. package/assets/band/board.png +0 -0
  38. package/assets/benchmark-3model.svg +21 -0
  39. package/assets/benchmark-agentic.svg +62 -0
  40. package/assets/codex-icon.png +0 -0
  41. package/assets/compare/code/after.webp +0 -0
  42. package/assets/compare/code/before.webp +0 -0
  43. package/assets/compare/text/after.webp +0 -0
  44. package/assets/compare/text/before.webp +0 -0
  45. package/assets/compare/ui/w-all.webp +0 -0
  46. package/assets/compare/ui/w-antislop.webp +0 -0
  47. package/assets/compare/ui/w-design.webp +0 -0
  48. package/assets/compare/ui/without.webp +0 -0
  49. package/assets/logo-dark.png +0 -0
  50. package/assets/logo-dark.svg +115 -0
  51. package/assets/logo-greenpt-dark.svg +27 -0
  52. package/assets/logo-greenpt.svg +27 -0
  53. package/assets/logo.png +0 -0
  54. package/assets/photoreunion-logo-dark.svg +4 -0
  55. package/assets/photoreunion-logo-light.svg +4 -0
  56. package/assets/profile.png +0 -0
  57. package/assets/retriever-logo-dark.svg +4 -0
  58. package/assets/retriever-logo-light.svg +4 -0
  59. package/assets/stories-banner.png +0 -0
  60. package/assets/v5/audit-es.png +0 -0
  61. package/assets/v5/audit-ja.png +0 -0
  62. package/assets/v5/audit-ko.png +0 -0
  63. package/assets/v5/audit-zh-CN.png +0 -0
  64. package/assets/v5/audit.png +0 -0
  65. package/assets/v5/beforeafter-es.png +0 -0
  66. package/assets/v5/beforeafter-ja.png +0 -0
  67. package/assets/v5/beforeafter-ko.png +0 -0
  68. package/assets/v5/beforeafter-zh-CN.png +0 -0
  69. package/assets/v5/beforeafter.png +0 -0
  70. package/assets/v5/chart-es.png +0 -0
  71. package/assets/v5/chart-ja.png +0 -0
  72. package/assets/v5/chart-ko.png +0 -0
  73. package/assets/v5/chart-zh-CN.png +0 -0
  74. package/assets/v5/chart.png +0 -0
  75. package/assets/v5/hero-es.jpg +0 -0
  76. package/assets/v5/hero-ja.jpg +0 -0
  77. package/assets/v5/hero-ko.jpg +0 -0
  78. package/assets/v5/hero-zh-CN.jpg +0 -0
  79. package/assets/v5/hero.jpg +0 -0
  80. package/assets/v5/ladder-es.png +0 -0
  81. package/assets/v5/ladder-ja.png +0 -0
  82. package/assets/v5/ladder-ko.png +0 -0
  83. package/assets/v5/ladder-zh-CN.png +0 -0
  84. package/assets/v5/ladder.png +0 -0
  85. package/assets/v5/review-es.png +0 -0
  86. package/assets/v5/review-ja.png +0 -0
  87. package/assets/v5/review-ko.png +0 -0
  88. package/assets/v5/review-zh-CN.png +0 -0
  89. package/assets/v5/review.png +0 -0
  90. package/assets/v5/tests-es.png +0 -0
  91. package/assets/v5/tests-ja.png +0 -0
  92. package/assets/v5/tests-ko.png +0 -0
  93. package/assets/v5/tests-zh-CN.png +0 -0
  94. package/assets/v5/tests.png +0 -0
  95. package/assets/waitlist-banner-es.png +0 -0
  96. package/assets/waitlist-banner-ja.png +0 -0
  97. package/assets/waitlist-banner-ko.png +0 -0
  98. package/assets/waitlist-banner-zh-CN.png +0 -0
  99. package/assets/waitlist-banner.png +0 -0
  100. package/benchmarks/.env.example +2 -0
  101. package/benchmarks/agentic/README.md +201 -0
  102. package/benchmarks/agentic/answer.py +288 -0
  103. package/benchmarks/agentic/compare.py +93 -0
  104. package/benchmarks/agentic/complete.py +178 -0
  105. package/benchmarks/agentic/judge.py +198 -0
  106. package/benchmarks/agentic/run.py +543 -0
  107. package/benchmarks/agentic/tasks.py +1544 -0
  108. package/benchmarks/arms/baseline.js +2 -0
  109. package/benchmarks/arms/caveman-SKILL.md +67 -0
  110. package/benchmarks/arms/caveman.js +8 -0
  111. package/benchmarks/arms/ponytail.js +8 -0
  112. package/benchmarks/behavior.js +54 -0
  113. package/benchmarks/behavior.yaml +40 -0
  114. package/benchmarks/correctness.js +291 -0
  115. package/benchmarks/loc.js +17 -19
  116. package/benchmarks/promptfooconfig.gemini.yaml +32 -0
  117. package/benchmarks/promptfooconfig.gpt-newest.yaml +33 -0
  118. package/benchmarks/promptfooconfig.yaml +41 -0
  119. package/benchmarks/results/2026-06-12-caveman-vs-ponytail.md +71 -0
  120. package/benchmarks/results/2026-06-12-v4-hardening-vs-caveman.md +125 -0
  121. package/benchmarks/results/2026-06-15-llama3.2-local.md +82 -0
  122. package/benchmarks/results/2026-06-16-correctness-gate-fix.md +113 -0
  123. package/benchmarks/results/2026-06-16-robustness-audit.md +135 -0
  124. package/benchmarks/results/2026-06-17-agentic-safety.md +164 -0
  125. package/benchmarks/results/2026-06-17-cost-verification.md +93 -0
  126. package/benchmarks/results/2026-06-18-agentic.md +219 -0
  127. package/benchmarks/results/2026-06-22-issue-245-217-comprehension.md +98 -0
  128. package/benchmarks/results/2026-10-07-agentic.md +186 -0
  129. package/cli/index.mjs +241 -0
  130. package/cli/lib/banner.mjs +21 -0
  131. package/cli/lib/install.mjs +498 -0
  132. package/cli/lib/settings.mjs +48 -0
  133. package/cli/package-lock.json +87 -0
  134. package/cli/package.json +42 -0
  135. package/cli/scripts/smoke-test.mjs +95 -0
  136. package/cli/scripts/smoke-worker.mjs +411 -0
  137. package/cli/scripts/sync-skills.mjs +81 -0
  138. package/commands/ponytail-audit.toml +2 -0
  139. package/commands/ponytail-debt.toml +2 -0
  140. package/commands/ponytail-gain.toml +2 -0
  141. package/commands/ponytail-help.toml +2 -0
  142. package/commands/ponytail-review.toml +2 -0
  143. package/commands/ponytail.toml +2 -0
  144. package/examples/email-validation.md +156 -0
  145. package/examples/group-by.md +35 -0
  146. package/examples/infinite-scroll.md +58 -0
  147. package/examples/number-formatting.md +37 -0
  148. package/examples/rate-limit.md +272 -0
  149. package/examples/react-countdown.md +390 -0
  150. package/examples/url-params.md +41 -0
  151. package/gemini-extension.json +2 -2
  152. package/hooks/band/board.ts +560 -0
  153. package/hooks/band/logo.ts +3 -0
  154. package/hooks/band/metrics.ts +311 -0
  155. package/hooks/band/register.tsx +495 -0
  156. package/hooks/band/types.d.ts +34 -0
  157. package/hooks/chill-statusline.ps1 +1 -1
  158. package/hooks/claude-band.json +6 -0
  159. package/hooks/claude-codex-hooks.json +34 -4
  160. package/hooks/copilot-hooks.json +12 -2
  161. package/hooks/cursor-hooks.json +2 -2
  162. package/hooks/ponytail-activate.js +201 -0
  163. package/hooks/ponytail-band-import.js +274 -0
  164. package/hooks/ponytail-config.js +175 -0
  165. package/hooks/ponytail-instructions.js +98 -0
  166. package/hooks/ponytail-map.js +111 -0
  167. package/hooks/ponytail-mode-tracker.js +216 -0
  168. package/hooks/ponytail-runtime.js +182 -0
  169. package/hooks/ponytail-statusline.ps1 +24 -0
  170. package/hooks/ponytail-statusline.sh +18 -0
  171. package/hooks/ponytail-subagent.js +94 -0
  172. package/hooks/qoder-hooks.json +19 -4
  173. package/i18n/README.ko.md +230 -0
  174. package/package.json +17 -6
  175. package/pi-extension/index.js +254 -0
  176. package/pi-extension/package.json +8 -0
  177. package/pi-extension/test/extension.test.js +367 -0
  178. package/pi-extension/test/helpers.test.js +179 -0
  179. package/plugin.yaml +21 -12
  180. package/rules/antislop.mdc +18 -0
  181. package/scripts/build-openclaw-skills.js +60 -0
  182. package/scripts/check-repo.mjs +389 -0
  183. package/scripts/check-rule-copies.js +74 -0
  184. package/scripts/check-versions.js +77 -0
  185. package/scripts/cursor-hooks.js +145 -0
  186. package/scripts/generate-chillslop.mjs +42 -0
  187. package/scripts/generate-examples.mjs +149 -0
  188. package/scripts/generate-plugin-doors.mjs +108 -0
  189. package/scripts/publish-openclaw-skills.js +75 -0
  190. package/scripts/skill-index.mjs +81 -0
  191. package/scripts/sync-ponytail.mjs +65 -0
  192. package/scripts/uninstall.js +92 -0
  193. package/skills/antislop/SKILL.md +700 -0
  194. package/skills/antislop/VERSION +1 -0
  195. package/skills/antislop-code/SKILL.md +128 -0
  196. package/skills/antislop-copywriting/SKILL.md +407 -0
  197. package/skills/antislop-human/LICENSE-web-quality-skills +21 -0
  198. package/skills/antislop-human/SKILL.md +204 -0
  199. package/skills/antislop-human/contrast-check.py +168 -0
  200. package/skills/antislop-human/contrast-mcp.py +121 -0
  201. package/skills/antislop-human/references/A11Y-PATTERNS.md +249 -0
  202. package/skills/antislop-human/references/WCAG.md +127 -0
  203. package/skills/antislop-layoutmobile/SKILL.md +170 -0
  204. package/skills/antislop-ui/LICENSE-web-quality-skills +21 -0
  205. package/skills/antislop-ui/SKILL.md +344 -0
  206. package/skills/antislop-ui/references/CORE-WEB-VITALS.md +181 -0
  207. package/skills/antislop-ui/references/WEB-BASELINE.md +28 -0
  208. package/skills/plugin.json +5 -0
  209. package/skills/ponytail/SKILL.md +52 -0
  210. package/skills/ponytail-audit/SKILL.md +103 -0
  211. package/skills/ponytail-debt/SKILL.md +44 -0
  212. package/skills/ponytail-gain/SKILL.md +52 -0
  213. package/skills/ponytail-help/SKILL.md +76 -0
  214. package/skills/ponytail-review/SKILL.md +101 -0
  215. package/src/package.json +3 -0
  216. package/tests/activation-map.test.js +63 -0
  217. package/tests/band-import.test.js +111 -0
  218. package/tests/band.test.tsx +520 -0
  219. package/tests/behavior.test.js +80 -0
  220. package/tests/commands.test.js +94 -0
  221. package/tests/copilot-plugin.test.js +35 -0
  222. package/tests/correctness.test.js +245 -0
  223. package/tests/cursor-hooks.test.js +343 -0
  224. package/tests/gemini-extension.test.js +91 -0
  225. package/tests/grok-plugin.test.js +39 -0
  226. package/tests/hermes-plugin.test.js +403 -0
  227. package/tests/hooks-windows.test.js +205 -0
  228. package/tests/hooks.test.js +1005 -0
  229. package/tests/kimi-plugin.test.js +25 -0
  230. package/tests/loc.test.js +18 -0
  231. package/tests/map.test.js +30 -0
  232. package/tests/openclaw-skills.test.js +26 -0
  233. package/tests/opencode-plugin.test.js +340 -0
  234. package/tests/package-scripts.test.js +35 -0
  235. package/tests/package.test.js +23 -0
  236. package/tests/qoder-plugin.test.js +84 -0
  237. package/tests/uninstall.test.js +202 -0
  238. /package/bin/{chill-agent.js → chill-agent.mjs} +0 -0
  239. /package/tests/{manifests.test.js → manifests.test.mjs} +0 -0
  240. /package/tests/{rules.test.js → rules.test.mjs} +0 -0
@@ -0,0 +1,2 @@
1
+ // Baseline arm: no skill, just the task.
2
+ module.exports = ({ vars }) => [{ role: 'user', content: vars.task }];
@@ -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
- #!/usr/bin/env node
2
- // benchmarks/loc.js - Lines of Code & Diff Length Efficiency Calculator
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
-
6
- export function countLoc(code) {
7
- const lines = code.split('\n')
8
- .map(l => l.trim())
9
- .filter(l => l.length > 0 && !l.startsWith('//') && !l.startsWith('/*') && !l.startsWith('*'));
10
- return lines.length;
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
- if (process.argv[2]) {
14
- const file = path.resolve(process.cwd(), process.argv[2]);
15
- if (fs.existsSync(file)) {
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." }