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,343 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Cursor adapter compatibility check (#817). The hooks must speak Cursor's
|
|
3
|
+
// hooks.json contract: camelCase events, JSON on stdout (empty stdout is a
|
|
4
|
+
// no-op, raw text is a parse error), `additional_context` on sessionStart, and
|
|
5
|
+
// `continue` plus `additional_context` on beforeSubmitPrompt. The installer must
|
|
6
|
+
// merge into an existing hooks.json without touching unrelated hooks.
|
|
7
|
+
//
|
|
8
|
+
// Input shapes mirror docs/cursor-hooks.md (Cursor docs and the 3.20.17
|
|
9
|
+
// client). This checks the adapter's input/output behavior only; whether the
|
|
10
|
+
// model actually receives the context is the live session check in that doc.
|
|
11
|
+
|
|
12
|
+
const test = require('node:test');
|
|
13
|
+
const assert = require('node:assert/strict');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { spawnSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
const root = path.join(__dirname, '..');
|
|
20
|
+
const rootFwd = root.replace(/\\/g, '/');
|
|
21
|
+
const TEMPLATE = 'hooks/cursor-hooks.json';
|
|
22
|
+
const { isPonytailHook } = require('../scripts/cursor-hooks');
|
|
23
|
+
|
|
24
|
+
// Keep host detection and config resolution deterministic whatever shell the
|
|
25
|
+
// suite runs in (a Cursor hook env, a Codex shell, a machine-wide default mode).
|
|
26
|
+
for (const key of [
|
|
27
|
+
'CURSOR_VERSION', 'CURSOR_PROJECT_DIR', 'CLAUDE_PROJECT_DIR', 'CLAUDE_PLUGIN_ROOT',
|
|
28
|
+
'CLAUDE_CONFIG_DIR', 'PLUGIN_DATA', 'COPILOT_PLUGIN_DATA', 'QODER_SESSION_ID',
|
|
29
|
+
'PONYTAIL_SUBAGENT_MATCHER', 'PONYTAIL_DEFAULT_MODE', 'XDG_CONFIG_HOME',
|
|
30
|
+
]) {
|
|
31
|
+
delete process.env[key];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ponytail-cursor-'));
|
|
35
|
+
process.on('exit', () => fs.rmSync(temp, { recursive: true, force: true }));
|
|
36
|
+
|
|
37
|
+
function run(script, env, input = '', cwd = undefined) {
|
|
38
|
+
return spawnSync(process.execPath, [path.join(root, 'hooks', script)], {
|
|
39
|
+
env: { ...process.env, ...env },
|
|
40
|
+
input,
|
|
41
|
+
cwd,
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function cli(args, env, cwd = undefined) {
|
|
47
|
+
return spawnSync(process.execPath, [path.join(root, 'scripts', 'cursor-hooks.js'), ...args], {
|
|
48
|
+
env: { ...process.env, ...env },
|
|
49
|
+
cwd,
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parse(result) {
|
|
55
|
+
assert.equal(result.status, 0, result.stderr);
|
|
56
|
+
assert.ok(result.stdout.startsWith('{'), `expected JSON on stdout, got: ${result.stdout.slice(0, 80)}`);
|
|
57
|
+
return JSON.parse(result.stdout);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// A fresh HOME and workspace per case so flag state cannot leak between cases.
|
|
61
|
+
// The env is what Cursor 3.20.17 gives a hook process: CURSOR_VERSION and the
|
|
62
|
+
// workspace root in CURSOR_PROJECT_DIR (plus its CLAUDE_PROJECT_DIR alias).
|
|
63
|
+
function cursorEnv(name, extra = {}) {
|
|
64
|
+
const home = path.join(temp, name, 'home');
|
|
65
|
+
const project = path.join(temp, name, 'project');
|
|
66
|
+
fs.mkdirSync(home, { recursive: true });
|
|
67
|
+
fs.mkdirSync(project, { recursive: true });
|
|
68
|
+
return {
|
|
69
|
+
home,
|
|
70
|
+
project,
|
|
71
|
+
flag: path.join(home, '.cursor', '.ponytail-active'),
|
|
72
|
+
env: {
|
|
73
|
+
HOME: home,
|
|
74
|
+
USERPROFILE: home,
|
|
75
|
+
XDG_CONFIG_HOME: path.join(home, '.config'),
|
|
76
|
+
CURSOR_VERSION: '3.20.17',
|
|
77
|
+
CURSOR_PROJECT_DIR: project,
|
|
78
|
+
CLAUDE_PROJECT_DIR: project,
|
|
79
|
+
...extra,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeFlag(c, mode) {
|
|
85
|
+
fs.mkdirSync(path.dirname(c.flag), { recursive: true });
|
|
86
|
+
fs.writeFileSync(c.flag, mode);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
test('cursor hooks template is a valid hooks.json with the two events that can inject context', () => {
|
|
90
|
+
const config = JSON.parse(fs.readFileSync(path.join(root, TEMPLATE), 'utf8'));
|
|
91
|
+
assert.equal(config.version, 1);
|
|
92
|
+
assert.deepEqual(Object.keys(config.hooks).sort(), ['beforeSubmitPrompt', 'sessionStart']);
|
|
93
|
+
assert.match(config.hooks.sessionStart[0].command, /ponytail-activate\.js/);
|
|
94
|
+
assert.match(config.hooks.beforeSubmitPrompt[0].command, /ponytail-mode-tracker\.js/);
|
|
95
|
+
// subagentStart answers only permission/user_message in Cursor, so it must
|
|
96
|
+
// not be registered: it would cost a process per subagent and inject nothing.
|
|
97
|
+
assert.equal(config.hooks.subagentStart, undefined);
|
|
98
|
+
for (const entries of Object.values(config.hooks)) {
|
|
99
|
+
assert.ok(Array.isArray(entries) && entries.length === 1);
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
assert.match(entry.command, /^node "PONYTAIL_DIR\/hooks\/ponytail-[\w-]+\.js"$/, 'plain node call, quoted absolute path');
|
|
102
|
+
assert.doesNotMatch(entry.command, /(^|\s)exec\s|&&|\|\|/, 'must run under cmd, PowerShell and bash alike');
|
|
103
|
+
assert.equal(typeof entry.timeout, 'number');
|
|
104
|
+
const script = entry.command.match(/hooks\/([\w.-]+\.js)/)[1];
|
|
105
|
+
assert.ok(fs.existsSync(path.join(root, 'hooks', script)), `command references a missing hook script: ${script}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('hook detection ignores later lookalike arguments after a real ponytail hook', () => {
|
|
111
|
+
assert.equal(isPonytailHook({
|
|
112
|
+
command: 'node "/opt/p/hooks/ponytail-activate.js" --log /tmp/hooks/ponytail-x/',
|
|
113
|
+
}), true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('isCursor is off outside a Cursor hook process', () => {
|
|
117
|
+
const { isCursor } = require('../hooks/ponytail-runtime');
|
|
118
|
+
assert.equal(isCursor, false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('sessionStart injects the default-level ruleset as additional_context and keeps state under ~/.cursor', () => {
|
|
122
|
+
const c = cursorEnv('start', { PONYTAIL_DEFAULT_MODE: 'ultra' });
|
|
123
|
+
const input = JSON.stringify({
|
|
124
|
+
hook_event_name: 'sessionStart', conversation_id: 'conv-1', session_id: 'conv-1',
|
|
125
|
+
is_background_agent: false, composer_mode: 'agent', workspace_roots: [c.project],
|
|
126
|
+
cursor_version: '3.20.17', model: 'claude-opus-4-7-thinking-max',
|
|
127
|
+
});
|
|
128
|
+
const output = parse(run('ponytail-activate.js', c.env, input));
|
|
129
|
+
assert.deepEqual(Object.keys(output), ['additional_context']);
|
|
130
|
+
assert.match(output.additional_context, /^PONYTAIL MODE ACTIVE — level: ultra/);
|
|
131
|
+
assert.match(output.additional_context, /Also question the request/, 'ultra row must survive the level filter');
|
|
132
|
+
assert.doesNotMatch(output.additional_context, /Build what was asked/, 'lite row must be filtered out');
|
|
133
|
+
assert.doesNotMatch(output.additional_context, /STATUSLINE SETUP NEEDED/, 'Cursor has no Claude statusline to nudge about');
|
|
134
|
+
assert.equal(fs.readFileSync(c.flag, 'utf8'), 'ultra');
|
|
135
|
+
assert.equal(fs.existsSync(path.join(c.home, '.claude')), false, 'Cursor state must not land in ~/.claude');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('sessionStart in off mode emits nothing and writes no flag', () => {
|
|
139
|
+
const c = cursorEnv('off', { PONYTAIL_DEFAULT_MODE: 'off' });
|
|
140
|
+
const result = run('ponytail-activate.js', c.env);
|
|
141
|
+
assert.equal(result.status, 0, result.stderr);
|
|
142
|
+
assert.equal(result.stdout, '', 'empty stdout is a no-op for Cursor; "OK" would be a JSON parse error');
|
|
143
|
+
assert.equal(fs.existsSync(c.flag), false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('Cursor running a Claude-format plugin (CLAUDE_PLUGIN_ROOT set) still gets Cursor JSON', () => {
|
|
147
|
+
const c = cursorEnv('plugin', { PONYTAIL_DEFAULT_MODE: 'full' });
|
|
148
|
+
const pluginRoot = path.join(c.home, '.cursor', 'plugins', 'ponytail');
|
|
149
|
+
c.env.CLAUDE_PLUGIN_ROOT = pluginRoot;
|
|
150
|
+
c.env.CURSOR_PLUGIN_ROOT = pluginRoot;
|
|
151
|
+
const output = parse(run('ponytail-activate.js', c.env));
|
|
152
|
+
assert.match(output.additional_context, /^PONYTAIL MODE ACTIVE — level: full/);
|
|
153
|
+
assert.equal(fs.readFileSync(c.flag, 'utf8'), 'full');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('beforeSubmitPrompt tracks /ponytail commands and delivers the new level ruleset', () => {
|
|
157
|
+
const c = cursorEnv('switch', { PONYTAIL_DEFAULT_MODE: 'full' });
|
|
158
|
+
writeFlag(c, 'full');
|
|
159
|
+
|
|
160
|
+
const sw = parse(run('ponytail-mode-tracker.js', c.env, JSON.stringify({
|
|
161
|
+
hook_event_name: 'beforeSubmitPrompt', conversation_id: 'conv-1',
|
|
162
|
+
prompt: '/ponytail lite', attachments: [],
|
|
163
|
+
})));
|
|
164
|
+
assert.equal(sw.continue, true, 'must never block the prompt');
|
|
165
|
+
assert.equal(sw.user_message, undefined, 'Cursor shows user_message only for blocked prompts');
|
|
166
|
+
assert.match(sw.additional_context, /^PONYTAIL MODE CHANGED — level: lite/);
|
|
167
|
+
assert.match(sw.additional_context, /Build what was asked/, 'Cursor has no /ponytail command, so the level ruleset rides along');
|
|
168
|
+
assert.doesNotMatch(sw.additional_context, /Also question the request/);
|
|
169
|
+
assert.equal(fs.readFileSync(c.flag, 'utf8'), 'lite');
|
|
170
|
+
|
|
171
|
+
// Bare /ponytail reports the live level without resetting it.
|
|
172
|
+
const report = parse(run('ponytail-mode-tracker.js', c.env, JSON.stringify({ prompt: '/ponytail' })));
|
|
173
|
+
assert.deepEqual(report, { continue: true, additional_context: 'PONYTAIL MODE ACTIVE — level: lite' });
|
|
174
|
+
assert.equal(fs.readFileSync(c.flag, 'utf8'), 'lite');
|
|
175
|
+
|
|
176
|
+
// /ponytail default persists the default without touching the session level.
|
|
177
|
+
const def = parse(run('ponytail-mode-tracker.js', c.env, JSON.stringify({ prompt: '/ponytail default ultra' })));
|
|
178
|
+
assert.equal(def.continue, true);
|
|
179
|
+
assert.match(def.additional_context, /PONYTAIL DEFAULT SET — new sessions start in ultra/);
|
|
180
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(c.home, '.config', 'ponytail', 'config.json'), 'utf8')).defaultMode, 'ultra');
|
|
181
|
+
assert.equal(fs.readFileSync(c.flag, 'utf8'), 'lite');
|
|
182
|
+
|
|
183
|
+
// /ponytail off and the plain-language deactivations clear the flag and tell the model.
|
|
184
|
+
const off = parse(run('ponytail-mode-tracker.js', c.env, JSON.stringify({ prompt: '/ponytail off' })));
|
|
185
|
+
assert.deepEqual(off, { continue: true, additional_context: 'PONYTAIL MODE OFF' });
|
|
186
|
+
assert.equal(fs.existsSync(c.flag), false);
|
|
187
|
+
|
|
188
|
+
writeFlag(c, 'full');
|
|
189
|
+
const stop = parse(run('ponytail-mode-tracker.js', c.env, JSON.stringify({ prompt: 'Stop ponytail.' })));
|
|
190
|
+
assert.equal(stop.additional_context, 'PONYTAIL MODE OFF');
|
|
191
|
+
assert.equal(fs.existsSync(c.flag), false);
|
|
192
|
+
|
|
193
|
+
// Ordinary prompts produce no output at all: Cursor treats empty stdout as "carry on".
|
|
194
|
+
writeFlag(c, 'full');
|
|
195
|
+
const plain = run('ponytail-mode-tracker.js', c.env, JSON.stringify({ prompt: 'add a normal mode toggle next to dark mode' }));
|
|
196
|
+
assert.equal(plain.status, 0, plain.stderr);
|
|
197
|
+
assert.equal(plain.stdout, '');
|
|
198
|
+
assert.equal(fs.readFileSync(c.flag, 'utf8'), 'full', 'incidental "normal mode" must not turn ponytail off');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('with the always-on rule in the workspace the hooks step back instead of duplicating the ruleset', () => {
|
|
202
|
+
const c = cursorEnv('rule', { PONYTAIL_DEFAULT_MODE: 'full' });
|
|
203
|
+
const rule = path.join(c.project, '.cursor', 'rules', 'ponytail.mdc');
|
|
204
|
+
fs.mkdirSync(path.dirname(rule), { recursive: true });
|
|
205
|
+
fs.copyFileSync(path.join(root, '.cursor', 'rules', 'ponytail.mdc'), rule);
|
|
206
|
+
|
|
207
|
+
const start = parse(run('ponytail-activate.js', c.env));
|
|
208
|
+
assert.match(start.additional_context, /always-on Cursor rule/);
|
|
209
|
+
assert.ok(start.additional_context.includes(rule), 'notice must name the rule file');
|
|
210
|
+
assert.doesNotMatch(start.additional_context, /PONYTAIL MODE ACTIVE/, 'no second copy of the ruleset');
|
|
211
|
+
assert.equal(fs.existsSync(c.flag), false, 'no mode flag while the rule owns the ruleset');
|
|
212
|
+
|
|
213
|
+
for (const prompt of ['/ponytail ultra', '/ponytail off', 'normal mode', '/ponytail']) {
|
|
214
|
+
const out = parse(run('ponytail-mode-tracker.js', c.env, JSON.stringify({ prompt })));
|
|
215
|
+
assert.equal(out.continue, true);
|
|
216
|
+
assert.match(out.additional_context, /always-on Cursor rule/, `${prompt} must answer with the rule notice`);
|
|
217
|
+
assert.doesNotMatch(out.additional_context, /PONYTAIL MODE (CHANGED|OFF|ACTIVE)/);
|
|
218
|
+
assert.equal(fs.existsSync(c.flag), false);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const plain = run('ponytail-mode-tracker.js', c.env, JSON.stringify({ prompt: 'hello' }));
|
|
222
|
+
assert.equal(plain.status, 0, plain.stderr);
|
|
223
|
+
assert.equal(plain.stdout, '', 'ordinary prompts stay silent');
|
|
224
|
+
|
|
225
|
+
// Project hooks run from the workspace root: the cwd fallback must find the rule too.
|
|
226
|
+
delete c.env.CURSOR_PROJECT_DIR;
|
|
227
|
+
delete c.env.CLAUDE_PROJECT_DIR;
|
|
228
|
+
const viaCwd = parse(run('ponytail-activate.js', c.env, '', c.project));
|
|
229
|
+
assert.match(viaCwd.additional_context, /always-on Cursor rule/);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('installer merges into an existing ~/.cursor/hooks.json and leaves unrelated hooks alone', () => {
|
|
233
|
+
const home = path.join(temp, 'install', 'home');
|
|
234
|
+
fs.mkdirSync(path.join(home, '.cursor'), { recursive: true });
|
|
235
|
+
const file = path.join(home, '.cursor', 'hooks.json');
|
|
236
|
+
const theirs = {
|
|
237
|
+
version: 1,
|
|
238
|
+
hooks: {
|
|
239
|
+
sessionStart: [{ command: './hooks/their-session.sh' }],
|
|
240
|
+
afterFileEdit: [{ command: './hooks/format.sh', matcher: 'Write' }],
|
|
241
|
+
stop: [{ command: 'node "./hooks/my-ponytail-logger.js"' }],
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
fs.writeFileSync(file, JSON.stringify(theirs));
|
|
245
|
+
const env = { HOME: home, USERPROFILE: home };
|
|
246
|
+
|
|
247
|
+
let result = cli(['install'], env);
|
|
248
|
+
assert.equal(result.status, 0, result.stderr);
|
|
249
|
+
let config = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
250
|
+
assert.equal(config.version, 1);
|
|
251
|
+
assert.deepEqual(config.hooks.afterFileEdit, theirs.hooks.afterFileEdit);
|
|
252
|
+
assert.deepEqual(config.hooks.stop, theirs.hooks.stop, 'similarly named user hook must survive install');
|
|
253
|
+
assert.equal(config.hooks.sessionStart[0].command, './hooks/their-session.sh', 'their sessionStart hook stays first and intact');
|
|
254
|
+
assert.equal(config.hooks.sessionStart.length, 2);
|
|
255
|
+
assert.deepEqual(config.hooks.sessionStart[1], { command: `node "${rootFwd}/hooks/ponytail-activate.js"`, timeout: 15 });
|
|
256
|
+
assert.deepEqual(config.hooks.beforeSubmitPrompt, [{ command: `node "${rootFwd}/hooks/ponytail-mode-tracker.js"`, timeout: 15 }]);
|
|
257
|
+
|
|
258
|
+
// Idempotent: a second install replaces ponytail's entries, never duplicates them.
|
|
259
|
+
result = cli(['install'], env);
|
|
260
|
+
assert.equal(result.status, 0, result.stderr);
|
|
261
|
+
config = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
262
|
+
assert.equal(config.hooks.sessionStart.length, 2);
|
|
263
|
+
assert.equal(config.hooks.beforeSubmitPrompt.length, 1);
|
|
264
|
+
|
|
265
|
+
// Uninstall removes only ponytail's entries.
|
|
266
|
+
result = cli(['uninstall'], env);
|
|
267
|
+
assert.equal(result.status, 0, result.stderr);
|
|
268
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(file, 'utf8')), theirs);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('installer creates the file when absent and removes it again when only ponytail lived there', () => {
|
|
272
|
+
const home = path.join(temp, 'fresh', 'home');
|
|
273
|
+
fs.mkdirSync(home, { recursive: true });
|
|
274
|
+
const env = { HOME: home, USERPROFILE: home };
|
|
275
|
+
const file = path.join(home, '.cursor', 'hooks.json');
|
|
276
|
+
|
|
277
|
+
let result = cli(['install'], env);
|
|
278
|
+
assert.equal(result.status, 0, result.stderr);
|
|
279
|
+
assert.match(result.stdout, /Installed ponytail hooks in /);
|
|
280
|
+
const config = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
281
|
+
assert.equal(config.version, 1);
|
|
282
|
+
assert.deepEqual(Object.keys(config.hooks).sort(), ['beforeSubmitPrompt', 'sessionStart']);
|
|
283
|
+
|
|
284
|
+
result = cli(['uninstall'], env);
|
|
285
|
+
assert.equal(result.status, 0, result.stderr);
|
|
286
|
+
assert.equal(fs.existsSync(file), false);
|
|
287
|
+
|
|
288
|
+
// Uninstalling on a clean machine is a no-op, not an error.
|
|
289
|
+
result = cli(['uninstall'], env);
|
|
290
|
+
assert.equal(result.status, 0, result.stderr);
|
|
291
|
+
assert.match(result.stdout, /No ponytail hooks/);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('installer --project writes <cwd>/.cursor/hooks.json', () => {
|
|
295
|
+
const home = path.join(temp, 'proj', 'home');
|
|
296
|
+
const project = path.join(temp, 'proj', 'repo');
|
|
297
|
+
fs.mkdirSync(home, { recursive: true });
|
|
298
|
+
fs.mkdirSync(project, { recursive: true });
|
|
299
|
+
const env = { HOME: home, USERPROFILE: home };
|
|
300
|
+
|
|
301
|
+
let result = cli(['install', '--project'], env, project);
|
|
302
|
+
assert.equal(result.status, 0, result.stderr);
|
|
303
|
+
const file = path.join(project, '.cursor', 'hooks.json');
|
|
304
|
+
assert.ok(fs.existsSync(file));
|
|
305
|
+
assert.equal(fs.existsSync(path.join(home, '.cursor', 'hooks.json')), false, '--project must not touch the user file');
|
|
306
|
+
|
|
307
|
+
result = cli(['uninstall', '--project'], env, project);
|
|
308
|
+
assert.equal(result.status, 0, result.stderr);
|
|
309
|
+
assert.equal(fs.existsSync(file), false);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('installer refuses to touch a malformed hooks.json', () => {
|
|
313
|
+
const home = path.join(temp, 'broken', 'home');
|
|
314
|
+
fs.mkdirSync(path.join(home, '.cursor'), { recursive: true });
|
|
315
|
+
const file = path.join(home, '.cursor', 'hooks.json');
|
|
316
|
+
const broken = '{ "version": 1, "hooks": { broken';
|
|
317
|
+
fs.writeFileSync(file, broken);
|
|
318
|
+
const env = { HOME: home, USERPROFILE: home };
|
|
319
|
+
|
|
320
|
+
for (const action of ['install', 'uninstall']) {
|
|
321
|
+
const result = cli([action], env);
|
|
322
|
+
assert.notEqual(result.status, 0, `${action} must fail on malformed JSON`);
|
|
323
|
+
assert.match(result.stderr, /not valid JSON/);
|
|
324
|
+
assert.equal(fs.readFileSync(file, 'utf8'), broken, 'malformed file must be left byte-for-byte intact');
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
for (const [name, config] of Object.entries({
|
|
329
|
+
'root-array': [{ command: 'keep me' }],
|
|
330
|
+
'hooks-array': { hooks: [{ command: 'keep me' }] },
|
|
331
|
+
'event-string': { hooks: { sessionStart: 'node custom-hook.js' } },
|
|
332
|
+
})) {
|
|
333
|
+
test(`installer refuses ${name} without overwriting hooks.json`, () => {
|
|
334
|
+
const c = cursorEnv(`invalid-${name}`);
|
|
335
|
+
const file = path.join(c.home, '.cursor', 'hooks.json');
|
|
336
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
337
|
+
const original = JSON.stringify(config, null, 2) + '\n';
|
|
338
|
+
fs.writeFileSync(file, original);
|
|
339
|
+
const result = cli(['install'], c.env);
|
|
340
|
+
assert.notEqual(result.status, 0, 'invalid config must not be silently rewritten');
|
|
341
|
+
assert.equal(fs.readFileSync(file, 'utf8'), original);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Smoke test for the Gemini CLI adapter. The adapter is a single thin manifest
|
|
3
|
+
// (gemini-extension.json) that reuses the repo's existing files: AGENTS.md for
|
|
4
|
+
// always-on context, commands/*.toml for /ponytail + /ponytail-review, and
|
|
5
|
+
// skills/ for the agent skills. This test fails if the manifest is removed,
|
|
6
|
+
// loses its pinned version, or points contextFileName at a file that no longer
|
|
7
|
+
// carries the load-bearing rules — i.e. if the adapter stops wiring ponytail.
|
|
8
|
+
|
|
9
|
+
const test = require('node:test');
|
|
10
|
+
const assert = require('node:assert/strict');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const root = path.join(__dirname, '..');
|
|
15
|
+
const MANIFEST = 'gemini-extension.json';
|
|
16
|
+
const EXTENSION_NAMES = ['chill-agent', 'ponytail'];
|
|
17
|
+
// Floating refs are a supply-chain footgun; the manifest version must be pinned.
|
|
18
|
+
const PINNED_SEMVER = /^\d+\.\d+\.\d+$/;
|
|
19
|
+
const VERSIONED_MANIFESTS = [
|
|
20
|
+
'gemini-extension.json',
|
|
21
|
+
'.claude-plugin/plugin.json',
|
|
22
|
+
'.codex-plugin/plugin.json',
|
|
23
|
+
'.github/plugin/plugin.json',
|
|
24
|
+
];
|
|
25
|
+
// Gemini auto-discovers these by directory; the manifest is only useful if they exist.
|
|
26
|
+
const REUSED_COMMANDS = ['commands/ponytail.toml', 'commands/ponytail-review.toml'];
|
|
27
|
+
const REUSED_SKILLS = ['skills/ponytail/SKILL.md'];
|
|
28
|
+
// Gemini CLI auto-loads this exact path for extension hooks. Ponytail's
|
|
29
|
+
// Claude/Codex hook map uses events Gemini does not support, so it must stay
|
|
30
|
+
// behind the host-specific plugin manifests instead.
|
|
31
|
+
const GEMINI_AUTO_HOOKS = 'hooks/hooks.json';
|
|
32
|
+
// Same load-bearing phrases asserted by scripts/check-rule-copies.js: the file
|
|
33
|
+
// contextFileName points at must actually carry the rules, not just exist.
|
|
34
|
+
const RULE_INVARIANTS = [
|
|
35
|
+
'lazy senior',
|
|
36
|
+
'validation at trust boundaries',
|
|
37
|
+
'shortcut: <the limit>, <when to upgrade>',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function read(relPath) {
|
|
41
|
+
return fs.readFileSync(path.join(root, relPath), 'utf8');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Read inside each test (not at module scope) so a missing or malformed manifest
|
|
45
|
+
// surfaces as a clean per-test assertion failure, not a load-time crash that
|
|
46
|
+
// collapses every case into one unreadable stack trace.
|
|
47
|
+
function loadManifest() {
|
|
48
|
+
assert.ok(fs.existsSync(path.join(root, MANIFEST)), `${MANIFEST} must exist`);
|
|
49
|
+
return JSON.parse(read(MANIFEST));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test('manifest names the ponytail extension with a pinned version', () => {
|
|
53
|
+
const manifest = loadManifest();
|
|
54
|
+
assert.ok(EXTENSION_NAMES.includes(manifest.name));
|
|
55
|
+
assert.match(manifest.version, PINNED_SEMVER);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('version stays aligned with the other plugin manifests', () => {
|
|
59
|
+
const versions = VERSIONED_MANIFESTS.map((rel) => {
|
|
60
|
+
const manifest = JSON.parse(read(rel));
|
|
61
|
+
assert.match(manifest.version, PINNED_SEMVER, `${rel} version must be pinned semver`);
|
|
62
|
+
return manifest.version;
|
|
63
|
+
});
|
|
64
|
+
const [sharedVersion, ...rest] = versions;
|
|
65
|
+
for (const version of rest) {
|
|
66
|
+
assert.equal(version, sharedVersion);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('contextFileName resolves to a file carrying the ponytail rules', () => {
|
|
71
|
+
const manifest = loadManifest();
|
|
72
|
+
assert.ok(manifest.contextFileName, 'contextFileName must be set so rules load every session');
|
|
73
|
+
const context = read(manifest.contextFileName);
|
|
74
|
+
for (const phrase of RULE_INVARIANTS) {
|
|
75
|
+
assert.ok(context.includes(phrase), `context file missing rule invariant: "${phrase}"`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('the commands and skills the adapter reuses are present', () => {
|
|
80
|
+
for (const rel of [...REUSED_COMMANDS, ...REUSED_SKILLS]) {
|
|
81
|
+
assert.ok(fs.existsSync(path.join(root, rel)), `reused file missing: ${rel}`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('Gemini cannot auto-discover Claude/Codex hook events', () => {
|
|
86
|
+
assert.equal(
|
|
87
|
+
fs.existsSync(path.join(root, GEMINI_AUTO_HOOKS)),
|
|
88
|
+
false,
|
|
89
|
+
`${GEMINI_AUTO_HOOKS} is auto-loaded by Gemini CLI; keep Claude/Codex hooks on manifest paths`,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Grok Build loads Ponytail through its native skill system. Lifecycle-hook
|
|
3
|
+
// stdout is passive in Grok, so this adapter must not register any hooks.
|
|
4
|
+
|
|
5
|
+
const test = require('node:test');
|
|
6
|
+
const assert = require('node:assert/strict');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const root = path.join(__dirname, '..');
|
|
11
|
+
|
|
12
|
+
test('Grok manifest is a skill-only adapter with no lifecycle hooks', () => {
|
|
13
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'plugin.json'), 'utf8'));
|
|
14
|
+
assert.ok(['chill-agent', 'ponytail'].includes(manifest.name));
|
|
15
|
+
assert.equal(manifest.hooks, undefined);
|
|
16
|
+
assert.equal(manifest.mcpServers, undefined);
|
|
17
|
+
assert.ok(!fs.existsSync(path.join(root, 'hooks', 'hooks.json')));
|
|
18
|
+
assert.ok(!fs.existsSync(path.join(root, '.grok-plugin', 'hooks.json')));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('Ponytail skill describes every coding task for Grok auto-invocation', () => {
|
|
22
|
+
const skill = fs.readFileSync(path.join(root, 'skills', 'ponytail', 'SKILL.md'), 'utf8');
|
|
23
|
+
assert.match(skill, /Use on any\s+coding task/i);
|
|
24
|
+
assert.match(skill, /writing,\s+fixing, refactoring, reviewing/i);
|
|
25
|
+
assert.doesNotMatch(skill, /disable-model-invocation:\s*true/i);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('Grok marketplace resolves a subdirectory with the same skills as direct installation', () => {
|
|
29
|
+
const catalog = JSON.parse(fs.readFileSync(path.join(root, '.grok-plugin', 'marketplace.json'), 'utf8'));
|
|
30
|
+
const entry = catalog.plugins.find((plugin) => plugin.name === 'ponytail');
|
|
31
|
+
const pluginRoot = path.resolve(root, entry.source);
|
|
32
|
+
// Grok rejects a marketplace source of "./", even though direct installation works.
|
|
33
|
+
assert.notEqual(pluginRoot, root, 'marketplace source must name a subdirectory');
|
|
34
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(pluginRoot, 'plugin.json'), 'utf8'));
|
|
35
|
+
assert.equal(manifest.name, entry.name);
|
|
36
|
+
assert.equal(manifest.hooks, undefined);
|
|
37
|
+
assert.equal(manifest.mcpServers, undefined);
|
|
38
|
+
assert.equal(path.resolve(pluginRoot, manifest.skills), path.join(root, 'skills'));
|
|
39
|
+
});
|