chill-agent 1.5.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 (187) hide show
  1. package/.claude-plugin/contrast-mcp-launcher.mjs +31 -0
  2. package/.claude-plugin/plugin.json +8 -3
  3. package/.cline-plugin/plugin.js +6 -0
  4. package/.codex-plugin/plugin.json +4 -3
  5. package/.cursor-plugin/marketplace.json +16 -0
  6. package/.cursor-plugin/plugin.json +2 -2
  7. package/.devin-plugin/plugin.json +2 -2
  8. package/.kimi-plugin/plugin.json +11 -29
  9. package/.omp-plugin/marketplace.json +15 -0
  10. package/.openclaw/skills/ponytail/SKILL.md +2 -7
  11. package/.openclaw/skills/ponytail-audit/SKILL.md +3 -6
  12. package/.openclaw/skills/ponytail-debt/SKILL.md +3 -4
  13. package/.openclaw/skills/ponytail-gain/SKILL.md +3 -4
  14. package/.openclaw/skills/ponytail-help/SKILL.md +3 -3
  15. package/.openclaw/skills/ponytail-review/SKILL.md +3 -6
  16. package/.opencode/plugins/ponytail.mjs +1 -1
  17. package/.qoder-plugin/plugin.json +12 -26
  18. package/AGENTS.md +32 -19
  19. package/__init__.py +1 -1
  20. package/assets/antislop-banner.png +0 -0
  21. package/assets/band/band.png +0 -0
  22. package/assets/band/board.png +0 -0
  23. package/assets/benchmark-3model.svg +21 -0
  24. package/assets/benchmark-agentic.svg +62 -0
  25. package/assets/codex-icon.png +0 -0
  26. package/assets/compare/code/after.webp +0 -0
  27. package/assets/compare/code/before.webp +0 -0
  28. package/assets/compare/text/after.webp +0 -0
  29. package/assets/compare/text/before.webp +0 -0
  30. package/assets/compare/ui/w-all.webp +0 -0
  31. package/assets/compare/ui/w-antislop.webp +0 -0
  32. package/assets/compare/ui/w-design.webp +0 -0
  33. package/assets/compare/ui/without.webp +0 -0
  34. package/assets/logo-dark.png +0 -0
  35. package/assets/logo-dark.svg +115 -0
  36. package/assets/logo-greenpt-dark.svg +27 -0
  37. package/assets/logo-greenpt.svg +27 -0
  38. package/assets/logo.png +0 -0
  39. package/assets/photoreunion-logo-dark.svg +4 -0
  40. package/assets/photoreunion-logo-light.svg +4 -0
  41. package/assets/profile.png +0 -0
  42. package/assets/retriever-logo-dark.svg +4 -0
  43. package/assets/retriever-logo-light.svg +4 -0
  44. package/assets/stories-banner.png +0 -0
  45. package/assets/v5/audit-es.png +0 -0
  46. package/assets/v5/audit-ja.png +0 -0
  47. package/assets/v5/audit-ko.png +0 -0
  48. package/assets/v5/audit-zh-CN.png +0 -0
  49. package/assets/v5/audit.png +0 -0
  50. package/assets/v5/beforeafter-es.png +0 -0
  51. package/assets/v5/beforeafter-ja.png +0 -0
  52. package/assets/v5/beforeafter-ko.png +0 -0
  53. package/assets/v5/beforeafter-zh-CN.png +0 -0
  54. package/assets/v5/beforeafter.png +0 -0
  55. package/assets/v5/chart-es.png +0 -0
  56. package/assets/v5/chart-ja.png +0 -0
  57. package/assets/v5/chart-ko.png +0 -0
  58. package/assets/v5/chart-zh-CN.png +0 -0
  59. package/assets/v5/chart.png +0 -0
  60. package/assets/v5/hero-es.jpg +0 -0
  61. package/assets/v5/hero-ja.jpg +0 -0
  62. package/assets/v5/hero-ko.jpg +0 -0
  63. package/assets/v5/hero-zh-CN.jpg +0 -0
  64. package/assets/v5/hero.jpg +0 -0
  65. package/assets/v5/ladder-es.png +0 -0
  66. package/assets/v5/ladder-ja.png +0 -0
  67. package/assets/v5/ladder-ko.png +0 -0
  68. package/assets/v5/ladder-zh-CN.png +0 -0
  69. package/assets/v5/ladder.png +0 -0
  70. package/assets/v5/review-es.png +0 -0
  71. package/assets/v5/review-ja.png +0 -0
  72. package/assets/v5/review-ko.png +0 -0
  73. package/assets/v5/review-zh-CN.png +0 -0
  74. package/assets/v5/review.png +0 -0
  75. package/assets/v5/tests-es.png +0 -0
  76. package/assets/v5/tests-ja.png +0 -0
  77. package/assets/v5/tests-ko.png +0 -0
  78. package/assets/v5/tests-zh-CN.png +0 -0
  79. package/assets/v5/tests.png +0 -0
  80. package/assets/waitlist-banner-es.png +0 -0
  81. package/assets/waitlist-banner-ja.png +0 -0
  82. package/assets/waitlist-banner-ko.png +0 -0
  83. package/assets/waitlist-banner-zh-CN.png +0 -0
  84. package/assets/waitlist-banner.png +0 -0
  85. package/benchmarks/.env.example +2 -0
  86. package/benchmarks/agentic/README.md +201 -0
  87. package/benchmarks/agentic/answer.py +288 -0
  88. package/benchmarks/agentic/compare.py +93 -0
  89. package/benchmarks/agentic/complete.py +178 -0
  90. package/benchmarks/agentic/judge.py +198 -0
  91. package/benchmarks/agentic/run.py +543 -0
  92. package/benchmarks/agentic/tasks.py +1544 -0
  93. package/benchmarks/arms/baseline.js +2 -0
  94. package/benchmarks/arms/caveman-SKILL.md +67 -0
  95. package/benchmarks/arms/caveman.js +8 -0
  96. package/benchmarks/arms/ponytail.js +8 -0
  97. package/benchmarks/behavior.js +54 -0
  98. package/benchmarks/behavior.yaml +40 -0
  99. package/benchmarks/correctness.js +291 -0
  100. package/benchmarks/loc.js +17 -19
  101. package/benchmarks/promptfooconfig.gemini.yaml +32 -0
  102. package/benchmarks/promptfooconfig.gpt-newest.yaml +33 -0
  103. package/benchmarks/promptfooconfig.yaml +41 -0
  104. package/benchmarks/results/2026-06-12-caveman-vs-ponytail.md +71 -0
  105. package/benchmarks/results/2026-06-12-v4-hardening-vs-caveman.md +125 -0
  106. package/benchmarks/results/2026-06-15-llama3.2-local.md +82 -0
  107. package/benchmarks/results/2026-06-16-correctness-gate-fix.md +113 -0
  108. package/benchmarks/results/2026-06-16-robustness-audit.md +135 -0
  109. package/benchmarks/results/2026-06-17-agentic-safety.md +164 -0
  110. package/benchmarks/results/2026-06-17-cost-verification.md +93 -0
  111. package/benchmarks/results/2026-06-18-agentic.md +219 -0
  112. package/benchmarks/results/2026-06-22-issue-245-217-comprehension.md +98 -0
  113. package/benchmarks/results/2026-10-07-agentic.md +186 -0
  114. package/cli/index.mjs +241 -0
  115. package/cli/lib/banner.mjs +21 -0
  116. package/cli/lib/install.mjs +498 -0
  117. package/cli/lib/settings.mjs +48 -0
  118. package/cli/package-lock.json +87 -0
  119. package/cli/package.json +42 -0
  120. package/cli/scripts/smoke-test.mjs +95 -0
  121. package/cli/scripts/smoke-worker.mjs +411 -0
  122. package/cli/scripts/sync-skills.mjs +81 -0
  123. package/examples/email-validation.md +156 -0
  124. package/examples/group-by.md +35 -0
  125. package/examples/infinite-scroll.md +58 -0
  126. package/examples/number-formatting.md +37 -0
  127. package/examples/rate-limit.md +272 -0
  128. package/examples/react-countdown.md +390 -0
  129. package/examples/url-params.md +41 -0
  130. package/gemini-extension.json +2 -2
  131. package/hooks/band/board.ts +560 -0
  132. package/hooks/band/logo.ts +3 -0
  133. package/hooks/band/metrics.ts +311 -0
  134. package/hooks/band/register.tsx +495 -0
  135. package/hooks/band/types.d.ts +34 -0
  136. package/hooks/claude-band.json +6 -0
  137. package/hooks/claude-codex-hooks.json +34 -4
  138. package/hooks/copilot-hooks.json +12 -2
  139. package/hooks/cursor-hooks.json +2 -2
  140. package/hooks/ponytail-band-import.js +274 -0
  141. package/hooks/ponytail-config.js +175 -0
  142. package/hooks/ponytail-map.js +111 -0
  143. package/hooks/ponytail-subagent.js +94 -0
  144. package/hooks/qoder-hooks.json +19 -4
  145. package/i18n/README.ko.md +230 -0
  146. package/package.json +12 -7
  147. package/pi-extension/test/extension.test.js +367 -0
  148. package/pi-extension/test/helpers.test.js +179 -0
  149. package/plugin.yaml +21 -12
  150. package/rules/antislop.mdc +18 -0
  151. package/scripts/build-openclaw-skills.js +60 -0
  152. package/scripts/check-repo.mjs +389 -0
  153. package/scripts/publish-openclaw-skills.js +75 -0
  154. package/scripts/skill-index.mjs +81 -0
  155. package/skills/antislop/SKILL.md +700 -0
  156. package/skills/antislop/VERSION +1 -0
  157. package/skills/antislop-code/SKILL.md +128 -0
  158. package/skills/antislop-copywriting/SKILL.md +407 -0
  159. package/skills/antislop-human/LICENSE-web-quality-skills +21 -0
  160. package/skills/antislop-human/SKILL.md +204 -0
  161. package/skills/antislop-human/contrast-check.py +168 -0
  162. package/skills/antislop-human/contrast-mcp.py +121 -0
  163. package/skills/antislop-human/references/A11Y-PATTERNS.md +249 -0
  164. package/skills/antislop-human/references/WCAG.md +127 -0
  165. package/skills/antislop-layoutmobile/SKILL.md +170 -0
  166. package/skills/antislop-ui/LICENSE-web-quality-skills +21 -0
  167. package/skills/antislop-ui/SKILL.md +344 -0
  168. package/skills/antislop-ui/references/CORE-WEB-VITALS.md +181 -0
  169. package/skills/antislop-ui/references/WEB-BASELINE.md +28 -0
  170. package/skills/plugin.json +5 -0
  171. package/src/package.json +3 -0
  172. package/tests/activation-map.test.js +63 -0
  173. package/tests/band-import.test.js +111 -0
  174. package/tests/band.test.tsx +520 -0
  175. package/tests/behavior.test.js +80 -0
  176. package/tests/gemini-extension.test.js +2 -2
  177. package/tests/grok-plugin.test.js +1 -1
  178. package/tests/hermes-plugin.test.js +1 -1
  179. package/tests/hooks-windows.test.js +205 -0
  180. package/tests/kimi-plugin.test.js +1 -1
  181. package/tests/map.test.js +30 -0
  182. package/tests/package-scripts.test.js +35 -0
  183. package/tests/package.test.js +23 -0
  184. package/tests/uninstall.test.js +202 -0
  185. /package/bin/{chill-agent.js → chill-agent.mjs} +0 -0
  186. /package/tests/{manifests.test.js → manifests.test.mjs} +0 -0
  187. /package/tests/{rules.test.js → rules.test.mjs} +0 -0
@@ -0,0 +1,367 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+
7
+ import ponytailExtension, { parsePonytailCommand } from "../index.js";
8
+
9
+ function createPiHarness() {
10
+ const events = new Map();
11
+ const commands = new Map();
12
+ const appendedEntries = [];
13
+ const sentUserMessages = [];
14
+
15
+ const pi = {
16
+ on(eventName, handler) {
17
+ events.set(eventName, handler);
18
+ },
19
+ registerCommand(name, options) {
20
+ commands.set(name, options);
21
+ },
22
+ appendEntry(customType, data) {
23
+ appendedEntries.push({ customType, data });
24
+ },
25
+ sendUserMessage(text, options) {
26
+ sentUserMessages.push({ text, options });
27
+ },
28
+ };
29
+
30
+ ponytailExtension(pi);
31
+ return { events, commands, appendedEntries, sentUserMessages };
32
+ }
33
+
34
+ function createCommandContext(overrides = {}) {
35
+ return {
36
+ isIdle: () => true,
37
+ sessionManager: { getEntries: () => [] },
38
+ ui: { notify() {} },
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ function withTempConfig(fn) {
44
+ const tempConfigHome = mkdtempSync(join(tmpdir(), "ponytail-test-"));
45
+ const previousXdg = process.env.XDG_CONFIG_HOME;
46
+ const previousHide = process.env.PONYTAIL_HIDE_STATUS;
47
+ const previousDefault = process.env.PONYTAIL_DEFAULT_MODE;
48
+ process.env.XDG_CONFIG_HOME = tempConfigHome;
49
+ delete process.env.PONYTAIL_HIDE_STATUS;
50
+ delete process.env.PONYTAIL_DEFAULT_MODE;
51
+
52
+ return Promise.resolve()
53
+ .then(fn)
54
+ .finally(() => {
55
+ if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
56
+ else process.env.XDG_CONFIG_HOME = previousXdg;
57
+ if (previousHide === undefined) delete process.env.PONYTAIL_HIDE_STATUS;
58
+ else process.env.PONYTAIL_HIDE_STATUS = previousHide;
59
+ if (previousDefault === undefined) delete process.env.PONYTAIL_DEFAULT_MODE;
60
+ else process.env.PONYTAIL_DEFAULT_MODE = previousDefault;
61
+ rmSync(tempConfigHome, { recursive: true, force: true });
62
+ });
63
+ }
64
+
65
+ test("extension registers Ponytail commands", () => {
66
+ const { commands } = createPiHarness();
67
+
68
+ assert.deepEqual([...commands.keys()].sort(), ["ponytail", "ponytail-audit", "ponytail-debt", "ponytail-gain", "ponytail-help", "ponytail-review"]);
69
+ });
70
+
71
+ test("/ponytail updates session mode and injects instructions", async () => withTempConfig(async () => {
72
+ const { commands, events, appendedEntries } = createPiHarness();
73
+ const ctx = createCommandContext();
74
+
75
+ await events.get("session_start")({ reason: "startup" }, ctx);
76
+ await commands.get("ponytail").handler("ultra", ctx);
77
+
78
+ assert.deepEqual(appendedEntries.at(-1), {
79
+ customType: "ponytail-mode",
80
+ data: { mode: "ultra" },
81
+ });
82
+
83
+ const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
84
+ assert.ok(result.systemPrompt.includes("PONYTAIL MODE ACTIVE"));
85
+ assert.ok(result.systemPrompt.includes("ultra"));
86
+ }));
87
+
88
+ test("before_agent_start guards missing event and missing systemPrompt (#439, #440)", async () => withTempConfig(async () => {
89
+ const { events } = createPiHarness();
90
+ const ctx = createCommandContext();
91
+ await events.get("session_start")({ reason: "startup" }, ctx); // currentMode -> default (full)
92
+
93
+ // #439: a null/undefined event must not crash, and still injects the ruleset.
94
+ for (const bad of [undefined, null]) {
95
+ const r = await events.get("before_agent_start")(bad, ctx);
96
+ assert.ok(r.systemPrompt.includes("PONYTAIL MODE ACTIVE"));
97
+ assert.ok(!r.systemPrompt.includes("undefined"), "must not contain the literal 'undefined'");
98
+ }
99
+
100
+ // #440: an event without a systemPrompt must not prepend the literal "undefined".
101
+ const empty = await events.get("before_agent_start")({}, ctx);
102
+ assert.ok(empty.systemPrompt.includes("PONYTAIL MODE ACTIVE"));
103
+ assert.ok(!empty.systemPrompt.startsWith("undefined"), "must not start with 'undefined'");
104
+
105
+ // A real base prompt is still preserved and prepended.
106
+ const withBase = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
107
+ assert.ok(withBase.systemPrompt.startsWith("BASE\n\n"));
108
+ assert.ok(withBase.systemPrompt.includes("PONYTAIL MODE ACTIVE"));
109
+ }));
110
+
111
+ test("before_agent_start keeps OMP prompt parts and uses Pi sections (#776, #953)", async () => withTempConfig(async () => {
112
+ const { commands, events } = createPiHarness();
113
+ const ctx = createCommandContext();
114
+ await events.get("session_start")({ reason: "startup" }, ctx);
115
+
116
+ // OMP: systemPrompt is an array of parts; they must stay separate, not comma-joined.
117
+ const omp = await events.get("before_agent_start")({ systemPrompt: ["A", "B"] }, ctx);
118
+ assert.deepEqual(omp.systemPrompt.slice(0, 2), ["A", "B"]);
119
+ assert.match(omp.systemPrompt[2], /PONYTAIL MODE ACTIVE/);
120
+
121
+ // Pi >= 0.86: write a section and leave the prompt alone; off removes it again.
122
+ const event = { systemPrompt: "BASE", systemPromptOptions: { sections: {} } };
123
+ assert.equal(await events.get("before_agent_start")(event, ctx), undefined);
124
+ assert.match(event.systemPromptOptions.sections.ponytail, /PONYTAIL MODE ACTIVE/);
125
+ await commands.get("ponytail").handler("off", ctx);
126
+ await events.get("before_agent_start")(event, ctx);
127
+ assert.equal(event.systemPromptOptions.sections.ponytail, undefined);
128
+ }));
129
+
130
+ test("session_start restores latest persisted mode", async () => withTempConfig(async () => {
131
+ const { events } = createPiHarness();
132
+ const ctx = createCommandContext({
133
+ sessionManager: {
134
+ getEntries: () => [
135
+ { type: "custom", customType: "ponytail-mode", data: { mode: "lite" } },
136
+ ],
137
+ },
138
+ });
139
+
140
+ await events.get("session_start")({ reason: "resume" }, ctx);
141
+ const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
142
+
143
+ assert.ok(result.systemPrompt.includes("lite"));
144
+ }));
145
+
146
+ test("session_tree restores the selected branch's mode without persisting it", async () => withTempConfig(async () => {
147
+ const { events, commands, appendedEntries } = createPiHarness();
148
+ const lite = { type: "custom", customType: "ponytail-mode", data: { mode: "lite" } };
149
+ const off = { type: "custom", customType: "ponytail-mode", data: { mode: "off" } };
150
+ let branch = [lite, off];
151
+ const statusWrites = [];
152
+ const notifications = [];
153
+ const ctx = createCommandContext({
154
+ sessionManager: { getBranch: () => branch, getEntries: () => [lite, off] },
155
+ ui: {
156
+ notify: (text) => notifications.push(text),
157
+ setStatus: (_key, text) => statusWrites.push(text),
158
+ theme: { fg: (_color, text) => text },
159
+ },
160
+ });
161
+
162
+ await events.get("session_start")({ reason: "resume" }, ctx);
163
+ await commands.get("ponytail").handler("default ultra", ctx);
164
+ assert.equal(await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx), undefined);
165
+ const notificationCount = notifications.length;
166
+
167
+ for (const [entries, mode] of [[[lite], "lite"], [[lite, off], "off"], [[], "ultra"]]) {
168
+ branch = entries;
169
+ await events.get("session_tree")?.({ type: "session_tree" }, ctx);
170
+ const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
171
+ if (mode === "off") {
172
+ assert.equal(result, undefined);
173
+ assert.equal(statusWrites.at(-1), undefined);
174
+ } else {
175
+ assert.ok(result?.systemPrompt.startsWith(`BASE\n\nPONYTAIL MODE ACTIVE — level: ${mode}\n`), `expected ${mode} instructions`);
176
+ assert.ok(statusWrites.at(-1).includes(mode.toUpperCase()));
177
+ }
178
+ }
179
+
180
+ assert.deepEqual(appendedEntries, [], "navigation must not append a new mode entry");
181
+ assert.equal(notifications.length, notificationCount, "navigation must not repeat the startup toast");
182
+ }));
183
+
184
+ test("OMP session_switch and session_branch restore the target session's mode", async () => withTempConfig(async () => {
185
+ // OMP keeps the legacy Pi events instead of re-emitting session_start on /new, /resume, /fork and /branch.
186
+ const { events, appendedEntries } = createPiHarness();
187
+ const lite = { type: "custom", customType: "ponytail-mode", data: { mode: "lite" } };
188
+ const off = { type: "custom", customType: "ponytail-mode", data: { mode: "off" } };
189
+ let branch = [off];
190
+ const ctx = createCommandContext({ sessionManager: { getBranch: () => branch } });
191
+
192
+ await events.get("session_start")({ type: "session_start" }, ctx);
193
+ for (const [type, entries, mode] of [["session_switch", [lite], "lite"], ["session_branch", [off], "off"], ["session_switch", [], "full"]]) {
194
+ branch = entries;
195
+ await events.get(type)?.({ type }, ctx);
196
+ const result = await events.get("before_agent_start")({ systemPrompt: ["BASE"] }, ctx);
197
+ if (mode === "off") assert.equal(result, undefined, `${type} should restore off`);
198
+ else assert.ok(result?.systemPrompt.at(-1).startsWith(`PONYTAIL MODE ACTIVE — level: ${mode}\n`), `${type} should restore ${mode}`);
199
+ }
200
+ assert.deepEqual(appendedEntries, [], "restoring must not append a new mode entry");
201
+ }));
202
+
203
+ test("skill alias commands delegate to Pi skill commands", async () => {
204
+ const { commands, sentUserMessages } = createPiHarness();
205
+ const ctx = createCommandContext();
206
+
207
+ await commands.get("ponytail-review").handler("", ctx);
208
+ await commands.get("ponytail-audit").handler("", ctx);
209
+ await commands.get("ponytail-debt").handler("", ctx);
210
+ await commands.get("ponytail-gain").handler("", ctx);
211
+ await commands.get("ponytail-help").handler("", ctx);
212
+
213
+ assert.deepEqual(sentUserMessages.map((entry) => entry.text), [
214
+ "/skill:ponytail-review",
215
+ "/skill:ponytail-audit",
216
+ "/skill:ponytail-debt",
217
+ "/skill:ponytail-gain",
218
+ "/skill:ponytail-help",
219
+ ]);
220
+ });
221
+
222
+ test("skill alias commands expand the skill and forward their arguments", async () => {
223
+ const { commands, sentUserMessages } = createPiHarness();
224
+
225
+ await commands.get("ponytail-review").handler("staged", createCommandContext());
226
+ await commands.get("ponytail-audit").handler("src/api", createCommandContext({ isIdle: () => false }));
227
+
228
+ assert.deepEqual(sentUserMessages, [
229
+ { text: "/skill:ponytail-review staged", options: { expandPromptTemplates: true } },
230
+ { text: "/skill:ponytail-audit src/api", options: { expandPromptTemplates: true, deliverAs: "followUp" } },
231
+ ]);
232
+ });
233
+
234
+ test("normal mode disables persistent instructions", async () => withTempConfig(async () => {
235
+ const { commands, events } = createPiHarness();
236
+ const ctx = createCommandContext();
237
+
238
+ await events.get("session_start")({ reason: "startup" }, ctx);
239
+ await commands.get("ponytail").handler("ultra", ctx);
240
+ await events.get("input")({ text: "normal mode", source: "interactive" }, ctx);
241
+
242
+ const disabled = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
243
+ assert.equal(disabled, undefined);
244
+ }));
245
+
246
+ test("a request mentioning normal mode stays active", async () => withTempConfig(async () => {
247
+ const { commands, events } = createPiHarness();
248
+ const ctx = createCommandContext();
249
+
250
+ await events.get("session_start")({ reason: "startup" }, ctx);
251
+ await commands.get("ponytail").handler("ultra", ctx);
252
+ await events.get("input")({ text: "add a normal mode toggle next to dark mode", source: "interactive" }, ctx);
253
+
254
+ const result = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
255
+ assert.match(result.systemPrompt, /PONYTAIL MODE ACTIVE/);
256
+ }));
257
+
258
+ test("status bar renders the mode and flips active on agent_start", async () => withTempConfig(async () => {
259
+ const { events } = createPiHarness();
260
+ const statusWrites = [];
261
+ const ctx = createCommandContext({
262
+ sessionManager: { getEntries: () => [{ type: "custom", customType: "ponytail-mode", data: { mode: "ultra" } }] },
263
+ ui: { notify() {}, setStatus: (key, text) => statusWrites.push({ key, text }), theme: { fg: (_color, text) => text } },
264
+ });
265
+
266
+ await events.get("session_start")({ reason: "resume" }, ctx);
267
+ await events.get("agent_start")({}, ctx);
268
+
269
+ assert.equal(statusWrites.at(-2).key, "ponytail");
270
+ assert.match(statusWrites.at(-2).text, /○.*ULTRA/);
271
+ assert.match(statusWrites.at(-1).text, /●.*ULTRA/);
272
+ }));
273
+
274
+ test("off mode clears the status bar entry", async () => withTempConfig(async () => {
275
+ const { events } = createPiHarness();
276
+ const statusWrites = [];
277
+ const ctx = createCommandContext({
278
+ sessionManager: { getEntries: () => [{ type: "custom", customType: "ponytail-mode", data: { mode: "off" } }] },
279
+ ui: { notify() {}, setStatus: (key, text) => statusWrites.push({ key, text }), theme: { fg: (_color, text) => text } },
280
+ });
281
+
282
+ await events.get("session_start")({ reason: "resume" }, ctx);
283
+
284
+ assert.deepEqual(statusWrites.at(-1), { key: "ponytail", text: undefined });
285
+ }));
286
+
287
+ test("status bar stays silent when ui lacks a theme", async () => withTempConfig(async () => {
288
+ const { events } = createPiHarness();
289
+ const calls = [];
290
+ const ctx = createCommandContext({
291
+ sessionManager: { getEntries: () => [{ type: "custom", customType: "ponytail-mode", data: { mode: "ultra" } }] },
292
+ ui: { notify() {}, setStatus: (_key, text) => calls.push(text) }, // setStatus present, theme absent
293
+ });
294
+
295
+ await events.get("session_start")({ reason: "resume" }, ctx);
296
+ await events.get("agent_start")({}, ctx);
297
+
298
+ assert.deepEqual(calls, []);
299
+ }));
300
+
301
+ test("PONYTAIL_HIDE_STATUS hides the indicator but keeps ponytail active (#324)", async () => withTempConfig(async () => {
302
+ process.env.PONYTAIL_HIDE_STATUS = "1";
303
+ const { events } = createPiHarness();
304
+ const statusWrites = [];
305
+ const ctx = createCommandContext({
306
+ sessionManager: { getEntries: () => [{ type: "custom", customType: "ponytail-mode", data: { mode: "ultra" } }] },
307
+ ui: { notify() {}, setStatus: (key, text) => statusWrites.push({ key, text }), theme: { fg: (_c, t) => t } },
308
+ });
309
+
310
+ await events.get("session_start")({ reason: "resume" }, ctx);
311
+ await events.get("agent_start")({}, ctx);
312
+ const injected = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
313
+
314
+ assert.deepEqual(statusWrites, [], "status bar must not be drawn when hidden");
315
+ assert.match(injected.systemPrompt, /PONYTAIL MODE ACTIVE/, "ruleset must still inject while status is hidden");
316
+ }));
317
+
318
+ test("config.hideStatus hides the indicator but keeps ponytail active (#324)", async () => withTempConfig(async () => {
319
+ mkdirSync(join(process.env.XDG_CONFIG_HOME, "ponytail"), { recursive: true });
320
+ writeFileSync(join(process.env.XDG_CONFIG_HOME, "ponytail", "config.json"), JSON.stringify({ hideStatus: true }));
321
+ const { events } = createPiHarness();
322
+ const statusWrites = [];
323
+ const ctx = createCommandContext({
324
+ ui: { notify() {}, setStatus: (key, text) => statusWrites.push({ key, text }), theme: { fg: (_c, t) => t } },
325
+ });
326
+
327
+ await events.get("session_start")({ reason: "startup" }, ctx);
328
+ await events.get("agent_start")({}, ctx);
329
+ const injected = await events.get("before_agent_start")({ systemPrompt: "BASE" }, ctx);
330
+
331
+ assert.deepEqual(statusWrites, [], "config.hideStatus must suppress the status bar");
332
+ assert.match(injected.systemPrompt, /PONYTAIL MODE ACTIVE/, "ruleset must still inject while status is hidden");
333
+ }));
334
+
335
+ test("PONYTAIL_HIDE_STATUS=0 does not hide the indicator", async () => withTempConfig(async () => {
336
+ process.env.PONYTAIL_HIDE_STATUS = "0";
337
+ const { events } = createPiHarness();
338
+ const statusWrites = [];
339
+ const ctx = createCommandContext({
340
+ sessionManager: { getEntries: () => [{ type: "custom", customType: "ponytail-mode", data: { mode: "ultra" } }] },
341
+ ui: { notify() {}, setStatus: (key, text) => statusWrites.push({ key, text }), theme: { fg: (_c, t) => t } },
342
+ });
343
+
344
+ await events.get("session_start")({ reason: "resume" }, ctx);
345
+ await events.get("agent_start")({}, ctx);
346
+
347
+ assert.ok(statusWrites.length > 0, "0 must be treated as 'do not hide'");
348
+ }));
349
+
350
+ test("/ponytail autocompletes modes, commands and default <mode>", () => {
351
+ const { commands } = createPiHarness();
352
+ const complete = (prefix) => commands.get("ponytail").getArgumentCompletions(prefix);
353
+
354
+ assert.deepEqual(complete("").map((item) => item.value),
355
+ ["off", "lite", "full", "ultra", "status", "default off", "default lite", "default full", "default ultra"]);
356
+ assert.deepEqual(complete("fu").map((item) => item.value), ["full"]);
357
+ assert.deepEqual(complete("default ").map((item) => item.value), ["default off", "default lite", "default full", "default ultra"]);
358
+ assert.deepEqual(complete("default l").map((item) => item.value), ["default lite"]);
359
+ assert.deepEqual(complete("default").map((item) => item.value),
360
+ ["default off", "default lite", "default full", "default ultra"], "never a bare default: Enter would submit it without a mode");
361
+ assert.equal(complete("status x"), null, "no command takes a second word after status");
362
+ assert.equal(complete("lite x"), null);
363
+ assert.equal(complete("bogus"), null, "nothing matches an unknown prefix");
364
+ for (const { value } of complete("")) {
365
+ assert.notEqual(parsePonytailCommand(value).type, "invalid", `Enter on "${value}" must run a valid command`);
366
+ }
367
+ });
@@ -0,0 +1,179 @@
1
+ import assert from "node:assert/strict";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+
7
+ import {
8
+ filterSkillBodyForMode,
9
+ parsePonytailCommand,
10
+ readDefaultMode,
11
+ readQuietStartup,
12
+ resolveSessionMode,
13
+ writeDefaultMode,
14
+ } from "../index.js";
15
+
16
+ test("parsePonytailCommand falls back to full when invoked bare and default is off", () => {
17
+ assert.deepEqual(parsePonytailCommand("", "off"), { type: "set-mode", mode: "full" });
18
+ // #639: when ponytail is already on, a bare command reports instead of resetting the level.
19
+ assert.deepEqual(parsePonytailCommand("", "full", "ultra"), { type: "status" });
20
+ assert.deepEqual(parsePonytailCommand("", "full", "off"), { type: "set-mode", mode: "full" });
21
+ });
22
+
23
+ test("parsePonytailCommand parses modes, status, and default subcommand", () => {
24
+ assert.deepEqual(parsePonytailCommand("ultra", "full"), { type: "set-mode", mode: "ultra" });
25
+ assert.deepEqual(parsePonytailCommand("status", "full"), { type: "status" });
26
+ assert.deepEqual(parsePonytailCommand("default lite", "full"), { type: "set-default", mode: "lite" });
27
+ });
28
+
29
+ test("parsePonytailCommand rejects review as a default (session-only mode, #377)", () => {
30
+ assert.deepEqual(parsePonytailCommand("default review", "full"), { type: "invalid", reason: "invalid-default-mode" });
31
+ });
32
+
33
+ test("resolveSessionMode still honors review as a session mode (not a default)", () => {
34
+ const entries = [{ type: "custom", customType: "ponytail-mode", data: { mode: "review" } }];
35
+ assert.equal(resolveSessionMode(entries, "full"), "review");
36
+ });
37
+
38
+ test("resolveSessionMode prefers latest persisted session mode", () => {
39
+ const entries = [
40
+ { type: "custom", customType: "ponytail-mode", data: { mode: "lite" } },
41
+ { type: "custom", customType: "ponytail-mode", data: { mode: "ultra" } },
42
+ ];
43
+
44
+ assert.equal(resolveSessionMode(entries, "full"), "ultra");
45
+ });
46
+
47
+ test("resolveSessionMode returns fallback when entries is not an array", () => {
48
+ assert.equal(resolveSessionMode(null, "ultra"), "ultra");
49
+ assert.equal(resolveSessionMode(undefined, "lite"), "lite");
50
+ assert.equal(resolveSessionMode({}, "full"), "full");
51
+ assert.equal(resolveSessionMode("not an array"), "full"); // DEFAULT_MODE fallback
52
+ });
53
+
54
+ test("readDefaultMode and writeDefaultMode use XDG config path", () => {
55
+ const tempDir = mkdtempSync(join(tmpdir(), "ponytail-config-"));
56
+ const previousXdg = process.env.XDG_CONFIG_HOME;
57
+ const previousDefault = process.env.PONYTAIL_DEFAULT_MODE;
58
+ const configPath = join(tempDir, "ponytail", "config.json");
59
+ process.env.XDG_CONFIG_HOME = tempDir;
60
+ delete process.env.PONYTAIL_DEFAULT_MODE;
61
+
62
+ try {
63
+ assert.equal(readDefaultMode(), "full");
64
+ assert.equal(writeDefaultMode("ultra"), "ultra");
65
+ assert.equal(readDefaultMode(), "ultra");
66
+ assert.ok(existsSync(configPath));
67
+ assert.deepEqual(JSON.parse(readFileSync(configPath, "utf8")), { defaultMode: "ultra" });
68
+ } finally {
69
+ if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
70
+ else process.env.XDG_CONFIG_HOME = previousXdg;
71
+ if (previousDefault === undefined) delete process.env.PONYTAIL_DEFAULT_MODE;
72
+ else process.env.PONYTAIL_DEFAULT_MODE = previousDefault;
73
+ rmSync(tempDir, { recursive: true, force: true });
74
+ }
75
+ });
76
+
77
+ test("readDefaultMode trims and normalizes whitespace in env and config values", () => {
78
+ const tempDir = mkdtempSync(join(tmpdir(), "ponytail-default-normalize-"));
79
+ const previousXdg = process.env.XDG_CONFIG_HOME;
80
+ const previousDefault = process.env.PONYTAIL_DEFAULT_MODE;
81
+ const configDir = join(tempDir, "ponytail");
82
+ const configPath = join(configDir, "config.json");
83
+ mkdirSync(configDir, { recursive: true });
84
+ process.env.XDG_CONFIG_HOME = tempDir;
85
+ process.env.PONYTAIL_DEFAULT_MODE = " FULL ";
86
+ writeFileSync(configPath, JSON.stringify({ defaultMode: "\tultra\n" }), "utf8");
87
+
88
+ try {
89
+ assert.equal(readDefaultMode(), "full");
90
+ process.env.PONYTAIL_DEFAULT_MODE = "\n lite \r\n";
91
+ assert.equal(readDefaultMode(), "lite");
92
+ delete process.env.PONYTAIL_DEFAULT_MODE;
93
+ assert.equal(readDefaultMode(), "ultra");
94
+ } finally {
95
+ if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
96
+ else process.env.XDG_CONFIG_HOME = previousXdg;
97
+ if (previousDefault === undefined) delete process.env.PONYTAIL_DEFAULT_MODE;
98
+ else process.env.PONYTAIL_DEFAULT_MODE = previousDefault;
99
+ rmSync(tempDir, { recursive: true, force: true });
100
+ }
101
+ });
102
+
103
+ test("readQuietStartup resolves env var, config file, and default in that order", () => {
104
+ const tempDir = mkdtempSync(join(tmpdir(), "ponytail-quiet-"));
105
+ const previousXdg = process.env.XDG_CONFIG_HOME;
106
+ const previousEnv = process.env.PONYTAIL_QUIET_STARTUP;
107
+ const configDir = join(tempDir, "ponytail");
108
+ const configPath = join(configDir, "config.json");
109
+ process.env.XDG_CONFIG_HOME = tempDir;
110
+ delete process.env.PONYTAIL_QUIET_STARTUP;
111
+
112
+ try {
113
+ // No env, no config -> default false (toast still shows)
114
+ assert.equal(readQuietStartup(), false);
115
+
116
+ // Config file true -> respected
117
+ mkdirSync(configDir, { recursive: true });
118
+ writeFileSync(configPath, JSON.stringify({ quietStartup: true }), "utf8");
119
+ assert.equal(readQuietStartup(), true);
120
+
121
+ // Env var overrides config
122
+ process.env.PONYTAIL_QUIET_STARTUP = "false";
123
+ assert.equal(readQuietStartup(), false);
124
+ process.env.PONYTAIL_QUIET_STARTUP = "1";
125
+ assert.equal(readQuietStartup(), true);
126
+ } finally {
127
+ if (previousXdg === undefined) delete process.env.XDG_CONFIG_HOME;
128
+ else process.env.XDG_CONFIG_HOME = previousXdg;
129
+ if (previousEnv === undefined) delete process.env.PONYTAIL_QUIET_STARTUP;
130
+ else process.env.PONYTAIL_QUIET_STARTUP = previousEnv;
131
+ rmSync(tempDir, { recursive: true, force: true });
132
+ }
133
+ });
134
+
135
+ test("filterSkillBodyForMode keeps only requested intensity examples and rows", () => {
136
+ // Examples are quoted in the real SKILL.md (`- lite: "..."`) — match that
137
+ // shape here too; see the next test for why the quote is load-bearing.
138
+ const body = `---\nname: ponytail\n---\n| **lite** | keep lite |\n| **full** | keep full |\n| **ultra** | keep ultra |\n- lite: "Lite example"\n- full: "Full example"\n- ultra: "Ultra example"\nOther line`;
139
+
140
+ const filtered = filterSkillBodyForMode(body, "ultra");
141
+
142
+ assert.ok(!filtered.includes("keep lite"));
143
+ assert.ok(!filtered.includes("keep full"));
144
+ assert.ok(filtered.includes("keep ultra"));
145
+ assert.ok(!filtered.includes("Lite example"));
146
+ assert.ok(filtered.includes("Ultra example"));
147
+ assert.ok(filtered.includes("Other line"));
148
+ });
149
+
150
+ test("filterSkillBodyForMode does not drop a rule bullet whose label matches a mode name", () => {
151
+ // A rule bullet like "- Full: ..." has the same "label: text" shape as a
152
+ // worked example, but isn't one — it must survive in every mode. Only the
153
+ // quoted, `- lite: "..."`-style bullets are real per-mode examples.
154
+ const body = `- Full: do not confuse this rule label with the mode name.\n- Lite: same risk, this is a real rule bullet.\n- lite: "real worked example"\n- ultra: "real worked example"`;
155
+
156
+ const filtered = filterSkillBodyForMode(body, "ultra");
157
+
158
+ assert.ok(filtered.includes("Full: do not confuse"), "an unquoted rule bullet must not be treated as a mode example");
159
+ assert.ok(filtered.includes("Lite: same risk"), "an unquoted rule bullet must not be treated as a mode example");
160
+ assert.ok(!filtered.includes("- lite:"), "the real quoted lite example must still be filtered out in ultra mode");
161
+ assert.ok(filtered.includes('ultra: "real worked example"'));
162
+ });
163
+
164
+ test("filterSkillBodyForMode keeps rule bullets that contain a colon", () => {
165
+ // Regression: rule bullets outside the Intensity section (e.g. the
166
+ // "No unrequested abstractions:" rule or the `shortcut:` comment convention)
167
+ // contain a colon and must not be mistaken for mode-example lines.
168
+ const skillPath = new URL("../../skills/ponytail/SKILL.md", import.meta.url);
169
+ const body = readFileSync(skillPath, "utf8");
170
+
171
+ const filtered = filterSkillBodyForMode(body, "full");
172
+
173
+ assert.ok(filtered.includes("Bug fix: before you edit, grep every caller"));
174
+ assert.ok(filtered.includes("Never cut: validation at trust boundaries"));
175
+ assert.ok(filtered.includes("code comment in this form: `shortcut: <the limit>, <when to upgrade>`"));
176
+ // The level rows are still filtered down to the active mode.
177
+ assert.ok(filtered.includes("| **full** |"));
178
+ assert.ok(!filtered.includes("| **lite** |"));
179
+ });
package/plugin.yaml CHANGED
@@ -1,12 +1,21 @@
1
- name: chill-agent
2
- version: 1.2.0
3
- description: ChillAgent Anti-Slop 2.0 & Pragmatic Senior Dev Standard
4
- skills:
5
- - ./skills/chill-slop/
6
- - ./skills/chill-slop-ui/
7
- - ./skills/chill-slop-code/
8
- - ./skills/chill-slop-copywriting/
9
- - ./skills/chill-slop-human/
10
- - ./skills/chill-slop-mobile/
11
- - ./skills/chill-audit/
12
- - ./skills/chill-review/
1
+ name: ponytail
2
+ version: 1.6.0
3
+ description: Lazy senior dev mode for Hermes Agent, always-on context, bundled skills, and slash commands.
4
+ author: sann
5
+ provides_hooks:
6
+ - pre_llm_call
7
+ - pre_gateway_dispatch
8
+ provides_commands:
9
+ - ponytail
10
+ - ponytail-review
11
+ - ponytail-audit
12
+ - ponytail-debt
13
+ - ponytail-gain
14
+ - ponytail-help
15
+ provides_skills:
16
+ - ponytail
17
+ - ponytail-review
18
+ - ponytail-audit
19
+ - ponytail-debt
20
+ - ponytail-gain
21
+ - ponytail-help
@@ -0,0 +1,18 @@
1
+ ---
2
+ description: Always-on antislop pointer for Cursor sessions. Loads the matching skills and saved usage mode.
3
+ globs: **/*
4
+ alwaysApply: true
5
+ ---
6
+
7
+ <EXTREMELY_IMPORTANT>
8
+ You use antislop (Anti Slop: Rules for AI Coding Agents). It is a filter, not a style guide: it stops generic AI slop in generated UI, copy, and code, without prescribing aesthetics.
9
+
10
+ For UI, copy, people, mobile layout, or code comments work, load the matching antislop skill before starting. Each skill is a folder in this plugin's skills/ directory (read its SKILL.md):
11
+ - Core filter, always on: skills/antislop/SKILL.md
12
+ - UI / visual: skills/antislop-ui/SKILL.md
13
+ - Copy & text: skills/antislop-copywriting/SKILL.md
14
+ - People: skills/antislop-human/SKILL.md
15
+ - Mobile / responsive: skills/antislop-layoutmobile/SKILL.md
16
+ - Code comments: skills/antislop-code/SKILL.md
17
+ Before starting, follow the core's "Two Usage Modes" section: explicit session instruction first, then global preference, then ask. For a resolved mode, announce `antislop active: <mode> (session override).` or `antislop active: <mode> (global preference).` once, using the actual mode and source. Ask only when no mode is resolved.
18
+ </EXTREMELY_IMPORTANT>
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ // Generate the OpenClaw / ClawHub skill package (.openclaw/skills/) from the
3
+ // canonical skills/. OpenClaw skills are SKILL.md (frontmatter + body), the same
4
+ // format ponytail already uses, with one difference: `description` must be a
5
+ // single line under 160 chars. The canonical descriptions are long (tuned for
6
+ // Claude's skill picker), so each ships a short one here. The body is copied
7
+ // verbatim from skills/<name>/SKILL.md so the ruleset never drifts; only the
8
+ // frontmatter is rewritten.
9
+ //
10
+ // Run: node scripts/build-openclaw-skills.js
11
+ // tests/openclaw-skills.test.js fails if the committed copies are stale.
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ const ROOT = path.join(__dirname, '..');
17
+ const HOMEPAGE = 'https://github.com/DietrichGebert/ponytail';
18
+
19
+ const DESCRIPTIONS = {
20
+ 'ponytail': 'Lazy senior dev mode for any coding task: the smallest change that fully solves it, and a reply a busy human understands in one read.',
21
+ 'ponytail-review': 'Quality review of a diff: bugs, security, real load, missing tests, speed, and what to delete. Each finding says what goes wrong and how to fix it.',
22
+ 'ponytail-audit': 'Quality audit of the whole repo: bugs, security, real load, missing tests, speed, and what to delete. Most important first.',
23
+ 'ponytail-debt': 'Harvest every shortcut: comment into one debt ledger, so deferrals get tracked instead of forgotten. One-shot report.',
24
+ 'ponytail-gain': 'Show ponytail measured impact as a scoreboard: less code, less cost, more speed, from the agentic benchmark averages. One-shot display.',
25
+ 'ponytail-help': "Quick reference for ponytail's modes, skills, and commands. One-shot display.",
26
+ };
27
+
28
+ const NAMES = Object.keys(DESCRIPTIONS);
29
+
30
+ function sourceBody(name) {
31
+ const src = fs.readFileSync(path.join(ROOT, 'skills', name, 'SKILL.md'), 'utf8').replace(/\r\n/g, '\n');
32
+ const fm = src.match(/^---\n[\s\S]*?\n---\n?/);
33
+ if (!fm) throw new Error(`skills/${name}/SKILL.md has no frontmatter`);
34
+ return src.slice(fm[0].length);
35
+ }
36
+
37
+ function render(name) {
38
+ const desc = DESCRIPTIONS[name];
39
+ if (desc.length > 160 || desc.includes('\n') || desc.includes('"')) {
40
+ throw new Error(`description for ${name} must be one line, no quotes, under 160 chars`);
41
+ }
42
+ const frontmatter =
43
+ `---\nname: ${name}\ndescription: "${desc}"\nhomepage: ${HOMEPAGE}\nlicense: MIT\n---\n`;
44
+ return frontmatter + sourceBody(name);
45
+ }
46
+
47
+ function outPath(name) {
48
+ return path.join(ROOT, '.openclaw', 'skills', name, 'SKILL.md');
49
+ }
50
+
51
+ module.exports = { DESCRIPTIONS, NAMES, render, outPath, sourceBody };
52
+
53
+ if (require.main === module) {
54
+ for (const name of NAMES) {
55
+ const p = outPath(name);
56
+ fs.mkdirSync(path.dirname(p), { recursive: true });
57
+ fs.writeFileSync(p, render(name));
58
+ console.log('wrote', path.relative(ROOT, p).replace(/\\/g, '/'));
59
+ }
60
+ }