chill-agent 1.4.0 → 1.5.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/marketplace.json +17 -0
- package/.clinerules/ponytail.md +30 -0
- package/.cursor/rules/ponytail.mdc +36 -0
- package/.grok-plugin/marketplace.json +16 -0
- package/.kiro/steering/ponytail.md +35 -0
- package/.openclaw/skills/ponytail/SKILL.md +52 -0
- package/.openclaw/skills/ponytail-audit/SKILL.md +103 -0
- package/.openclaw/skills/ponytail-debt/SKILL.md +44 -0
- package/.openclaw/skills/ponytail-gain/SKILL.md +52 -0
- package/.openclaw/skills/ponytail-help/SKILL.md +76 -0
- package/.openclaw/skills/ponytail-review/SKILL.md +101 -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/.windsurf/rules/ponytail.md +30 -0
- package/AGENTS.md +19 -0
- package/__init__.py +228 -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/hooks/chill-statusline.ps1 +1 -1
- package/hooks/ponytail-activate.js +201 -0
- package/hooks/ponytail-instructions.js +98 -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/package.json +9 -3
- package/pi-extension/index.js +254 -0
- package/pi-extension/package.json +8 -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/sync-ponytail.mjs +65 -0
- package/scripts/uninstall.js +92 -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/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.test.js +1005 -0
- package/tests/kimi-plugin.test.js +25 -0
- package/tests/loc.test.js +18 -0
- package/tests/openclaw-skills.test.js +26 -0
- package/tests/opencode-plugin.test.js +340 -0
- package/tests/qoder-plugin.test.js +84 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Quality review of the current changes: bugs, security, load, tests, speed, bloat
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Review the current code changes like the senior developer who will be paged when they break. Review what the user names (uncommitted or staged changes, a branch, a PR link, files); nothing named: the uncommitted changes, or the last commit if there are none. First read the code the change touches: callers of every changed function, the functions it calls, the tests, the README (find the expected load: one person or many users and processes). A change can break code it does not touch. Look for, in this order: bugs (wrong result, crash, missed edge case, a caller broken by the change, a fix in one caller while the shared function stays broken), risk (security holes, data loss), scale (check-then-write races, the same work done by every process, memory that only grows, a query per item, O(n^2) on big input), risky logic without a test, slow code (big = problem, small = suggestion), lean (dead code, a helper the repo already has, stdlib or native instead of custom code or a new dependency, abstractions with one implementation, near-copies to merge, a function doing several unrelated jobs to split, never split by line count). Every finding needs a concrete case; re-read the lines to confirm it. A shortcut marked with a shortcut: (or older ponytail:) comment that names its limit is a decision, not a finding. Propose the smallest fix, prefer deleting code, never add layers. No style taste, no vague worries. Very simple English, explain technical terms. Start with 'What this change does:' in two or three sentences and the load you assumed. Then numbered findings grouped Must fix / Should fix / Nice to have; each finding: a bold title with file:lines, then four bullets: What this is, Problem, Fix, If we skip it (one or two short sentences each). End with 'Verdict:', 'Lean: -N lines possible.' when lean findings exist, and 'Not checked:' if something mattered and you could not check it. Nothing found: 'Looks good. Ship.' Change no code.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Switch ponytail intensity level (lite/full/ultra/off)
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Switch to ponytail $ARGUMENTS mode. With no level, keep the current level, or switch on at the default level if ponytail was off. Lazy senior dev mode, before any code: does it need to exist at all (YAGNI)? Does the standard library do it? A native platform feature? Can it be one line? Build the minimum that works. No unrequested abstractions, no avoidable dependencies, no boilerplate. Mark deliberate simplifications that cut a real corner with a known ceiling using a shortcut: comment that names the ceiling and upgrade path.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// OpenCode 2 loads this file automatically from .opencode/plugins/ and does not
|
|
2
|
+
// read ponytail.mjs next to it, so keep the one-line entry here. npm consumers
|
|
3
|
+
// never see it: package.json main/exports still point at ponytail.mjs.
|
|
4
|
+
export { default } from './ponytail.mjs'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ponytail command-file and SKILL.md frontmatter parser.
|
|
4
|
+
//
|
|
5
|
+
// Pulled out of ponytail.mjs so the plugin module's only top-level export is
|
|
6
|
+
// the plugin definition itself. OpenCode's legacy plugin loader (the one that
|
|
7
|
+
// runs before v1 plugins are detected) treats every function exported from a
|
|
8
|
+
// plugin module as a plugin; calling the frontmatter parser as one threw
|
|
9
|
+
// "path must be a string or a file descriptor" because it got the plugin
|
|
10
|
+
// context object as its first argument. Keeping the parser in its own module
|
|
11
|
+
// leaves exactly one plugin-shaped export on ponytail.mjs.
|
|
12
|
+
|
|
13
|
+
function parseCommandFile(filePath) {
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
16
|
+
// Tolerate CRLF: a Windows checkout (autocrlf) delivers \r\n, npm ships \n.
|
|
17
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
18
|
+
if (!match) return null;
|
|
19
|
+
const description = match[1].match(/description:\s*(.+)/)?.[1]?.trim();
|
|
20
|
+
return { description, template: match[2].trim() };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ponytail: flat frontmatter only — the shape every shipped SKILL.md uses. A
|
|
24
|
+
// nested key or an anchor is not interpreted, and a real YAML parser is the
|
|
25
|
+
// upgrade path if a skill ever needs one.
|
|
26
|
+
function frontmatterField(frontmatter, key) {
|
|
27
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
28
|
+
const at = lines.findIndex((line) => line.startsWith(key + ':'));
|
|
29
|
+
if (at === -1) return undefined;
|
|
30
|
+
const value = lines[at].slice(key.length + 1).trim();
|
|
31
|
+
if (value[0] !== '>' && value[0] !== '|') return value;
|
|
32
|
+
|
|
33
|
+
// Block scalar: `>` folds the lines into one, `|` keeps them. Indented lines
|
|
34
|
+
// continue the block, the next unindented key ends it (skills/ponytail has
|
|
35
|
+
// `argument-hint` after its description), and a trailing `-` drops the
|
|
36
|
+
// newline YAML would otherwise close the scalar with.
|
|
37
|
+
const block = [];
|
|
38
|
+
for (const line of lines.slice(at + 1)) {
|
|
39
|
+
if (line.trim() && !/^\s/.test(line)) break;
|
|
40
|
+
block.push(line.trim());
|
|
41
|
+
}
|
|
42
|
+
while (block.length && !block[block.length - 1]) block.pop();
|
|
43
|
+
const joined = block.join(value[0] === '|' ? '\n' : ' ');
|
|
44
|
+
return value.endsWith('-') ? joined : joined + '\n';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// OpenCode's V2 skill transform takes the resolved fields, not the file: the
|
|
48
|
+
// skill id is the directory name, and `content` is the body with its
|
|
49
|
+
// frontmatter stripped. Matches what OpenCode itself derives from a discovered
|
|
50
|
+
// .opencode/skills/<id>/SKILL.md.
|
|
51
|
+
function parseSkillFile(filePath) {
|
|
52
|
+
const fs = require('fs');
|
|
53
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
54
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---[^\S\n]*\r?\n?([\s\S]*)$/);
|
|
55
|
+
if (!match) return null;
|
|
56
|
+
return {
|
|
57
|
+
name: frontmatterField(match[1], 'name'),
|
|
58
|
+
description: frontmatterField(match[1], 'description'),
|
|
59
|
+
body: match[2],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { parseCommandFile, parseSkillFile };
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// ponytail — OpenCode plugin.
|
|
2
|
+
//
|
|
3
|
+
// Injects the ponytail ruleset into every chat's system prompt at the active
|
|
4
|
+
// intensity, persists /ponytail mode switches, and registers the /ponytail
|
|
5
|
+
// commands and skills so they work when the package is installed from npm.
|
|
6
|
+
// Reuses the shared instruction builder so Claude Code, Codex, pi, and OpenCode
|
|
7
|
+
// all read one source of truth.
|
|
8
|
+
//
|
|
9
|
+
// OpenCode loads this as a server plugin — add it to your opencode.json:
|
|
10
|
+
// { "plugins": ["@dietrichgebert/ponytail"] }
|
|
11
|
+
//
|
|
12
|
+
// One default export serves both plugin APIs: V2 reads `id` + `setup`, V1 calls
|
|
13
|
+
// `server()`.
|
|
14
|
+
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
|
|
23
|
+
// The shared instruction builder is CommonJS; bridge to it from this ES module.
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const { getPonytailInstructions } = require('../../hooks/ponytail-instructions');
|
|
26
|
+
const { getDefaultMode, normalizeMode, writeDefaultMode } = require('../../hooks/ponytail-config');
|
|
27
|
+
const { parseCommandFile, parseSkillFile } = require('./ponytail-frontmatter.cjs');
|
|
28
|
+
|
|
29
|
+
// OpenCode has no flag-file convention of its own; keep mode beside its config.
|
|
30
|
+
// Shared with the V1 path, so a level set under either API is read by both.
|
|
31
|
+
const statePath = path.join(
|
|
32
|
+
process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
|
|
33
|
+
'opencode',
|
|
34
|
+
'.ponytail-active',
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
function readMode() {
|
|
38
|
+
try {
|
|
39
|
+
// review is a one-shot command, never a level (#736); an old saved review falls back.
|
|
40
|
+
return normalizeMode(fs.readFileSync(statePath, 'utf8')) || getDefaultMode();
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return getDefaultMode();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function writeMode(mode) {
|
|
47
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
48
|
+
fs.writeFileSync(statePath, mode);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// `off` is persisted like any mode; the injection reads it and stays silent.
|
|
52
|
+
// An unrecognized level leaves the current one alone. The write lands before
|
|
53
|
+
// the next turn's injection, not the current one — good enough; switch to a
|
|
54
|
+
// synchronous store if same-turn switching ever matters.
|
|
55
|
+
function persistMode(args) {
|
|
56
|
+
const wanted = String(args == null ? '' : args).trim();
|
|
57
|
+
// `/ponytail default <level>` saves the default for new sessions, like the Claude hook.
|
|
58
|
+
const [first, second] = wanted.toLowerCase().split(/\s+/);
|
|
59
|
+
if (first === 'default') {
|
|
60
|
+
const saved = writeDefaultMode(second);
|
|
61
|
+
if (saved) console.log('ponytail default ' + saved);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Bare /ponytail switches ponytail on, or keeps the level when it already is (#639).
|
|
65
|
+
if (!wanted && readMode() !== 'off') return;
|
|
66
|
+
const mode = wanted ? normalizeMode(wanted) : (getDefaultMode() === 'off' ? 'full' : getDefaultMode());
|
|
67
|
+
if (!mode) return;
|
|
68
|
+
writeMode(mode);
|
|
69
|
+
console.log('ponytail ' + mode);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// V2 has no `config` hook: the domains that own these definitions own them now,
|
|
73
|
+
// so the command and skill files are read once here and handed to the
|
|
74
|
+
// transforms that register them.
|
|
75
|
+
function readCommands() {
|
|
76
|
+
const dir = path.join(__dirname, '..', 'command');
|
|
77
|
+
try {
|
|
78
|
+
return fs.readdirSync(dir)
|
|
79
|
+
.filter((file) => file.endsWith('.md'))
|
|
80
|
+
.map((file) => {
|
|
81
|
+
const parsed = parseCommandFile(path.join(dir, file));
|
|
82
|
+
return parsed && { name: path.basename(file, '.md'), ...parsed };
|
|
83
|
+
})
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readSkills() {
|
|
91
|
+
const dir = path.resolve(__dirname, '../../skills');
|
|
92
|
+
try {
|
|
93
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
94
|
+
.filter((entry) => entry.isDirectory())
|
|
95
|
+
.map((entry) => {
|
|
96
|
+
const file = path.join(dir, entry.name, 'SKILL.md');
|
|
97
|
+
const parsed = parseSkillFile(file);
|
|
98
|
+
return parsed && {
|
|
99
|
+
id: entry.name,
|
|
100
|
+
name: parsed.name || entry.name,
|
|
101
|
+
description: parsed.description,
|
|
102
|
+
path: file,
|
|
103
|
+
content: parsed.body,
|
|
104
|
+
};
|
|
105
|
+
})
|
|
106
|
+
.filter(Boolean);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Templates without $ARGUMENTS would drop what the user typed after the
|
|
113
|
+
// command (/ponytail-review feature/login), so append it instead.
|
|
114
|
+
function withArguments(template, input) {
|
|
115
|
+
const text = String(input == null ? '' : input);
|
|
116
|
+
if (template.includes('$ARGUMENTS')) return template.replaceAll('$ARGUMENTS', text);
|
|
117
|
+
return text.trim() ? template + '\n\n' + text.trim() : template;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default {
|
|
121
|
+
id: 'ponytail',
|
|
122
|
+
|
|
123
|
+
async setup(ctx) {
|
|
124
|
+
const commands = readCommands();
|
|
125
|
+
const skills = readSkills();
|
|
126
|
+
|
|
127
|
+
await ctx.skill.transform((editor) => {
|
|
128
|
+
for (const skill of skills) editor.add(skill);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await ctx.command.transform((editor) => {
|
|
132
|
+
for (const command of commands) {
|
|
133
|
+
editor.add({
|
|
134
|
+
name: command.name,
|
|
135
|
+
description: command.description,
|
|
136
|
+
execute: async ({ sessionID, prompt, delivery }) => {
|
|
137
|
+
if (command.name === 'ponytail') persistMode(prompt.text);
|
|
138
|
+
const withoutMentions = (items) => items?.map(({ mention, ...item }) => item);
|
|
139
|
+
await ctx.session.prompt({
|
|
140
|
+
...prompt,
|
|
141
|
+
sessionID,
|
|
142
|
+
text: withArguments(command.template, prompt.text),
|
|
143
|
+
files: withoutMentions(prompt.files),
|
|
144
|
+
agents: withoutMentions(prompt.agents),
|
|
145
|
+
skills: withoutMentions(prompt.skills),
|
|
146
|
+
delivery,
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Append the ruleset to the system prompt every turn. V2 hands over owned
|
|
154
|
+
// system parts, so push one instead of rewriting the tail of another.
|
|
155
|
+
// Summary compaction runs its own hook with the same event shape; without
|
|
156
|
+
// it the compaction request misses the ruleset and the provider prompt cache.
|
|
157
|
+
const addRuleset = (event) => {
|
|
158
|
+
const mode = readMode();
|
|
159
|
+
if (mode === 'off') return;
|
|
160
|
+
event.system.push({ type: 'text', text: getPonytailInstructions(mode) });
|
|
161
|
+
};
|
|
162
|
+
await ctx.session.hook('context', addRuleset);
|
|
163
|
+
await ctx.session.hook('compaction', addRuleset);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// OpenCode V1: same three behaviors, as hooks on the V1 hook names.
|
|
167
|
+
async server({ client } = {}) {
|
|
168
|
+
const log = (level, message) => {
|
|
169
|
+
try { client && client.app && client.app.log({ body: { service: 'ponytail', level, message } }); } catch (e) {}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
// Register slash commands + skills directory.
|
|
174
|
+
config: async (config) => {
|
|
175
|
+
if (!config.command) config.command = {};
|
|
176
|
+
for (const command of readCommands()) {
|
|
177
|
+
config.command[command.name] = { description: command.description, template: command.template };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
config.skills = config.skills || {};
|
|
181
|
+
config.skills.paths = config.skills.paths || [];
|
|
182
|
+
const ponytailSkillsDir = path.resolve(__dirname, '../../skills');
|
|
183
|
+
if (!config.skills.paths.includes(ponytailSkillsDir)) {
|
|
184
|
+
config.skills.paths.push(ponytailSkillsDir);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
'experimental.chat.system.transform': async (_input, output) => {
|
|
189
|
+
const mode = readMode();
|
|
190
|
+
if (mode === 'off') return;
|
|
191
|
+
const instructions = getPonytailInstructions(mode);
|
|
192
|
+
if (output.system.length > 0) {
|
|
193
|
+
output.system[output.system.length - 1] += '\n\n' + instructions;
|
|
194
|
+
} else {
|
|
195
|
+
output.system.push(instructions);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
'command.execute.before': async (input) => {
|
|
200
|
+
if (!input || input.command !== 'ponytail') return;
|
|
201
|
+
persistMode(input.arguments);
|
|
202
|
+
log('info', 'ponytail ' + readMode());
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Ponytail, lazy senior dev mode
|
|
2
|
+
|
|
3
|
+
You are a lazy senior developer. The best code is the code never written. You solve the whole problem with the least new code. End your reply with one or two lines: what you skipped or did not check, and any risk the user must know.
|
|
4
|
+
|
|
5
|
+
## Before you write
|
|
6
|
+
|
|
7
|
+
Read the task and the code it touches. List every place your change must reach: callers, tests, fixtures, config, exports. Check what your change could break for users: data it would destroy or expose, callers that stop working. That is scope. Extra features are not.
|
|
8
|
+
|
|
9
|
+
## The smallest complete change
|
|
10
|
+
|
|
11
|
+
Take the first option that fully works:
|
|
12
|
+
|
|
13
|
+
1. Does it need to exist? Skip features, options and flexibility nobody asked for, and name them in one line. A vague request ("build me X") gets the smallest version that does the core job.
|
|
14
|
+
2. Already in this codebase (a helper, component, service, pattern)? Use it the way the surrounding code does.
|
|
15
|
+
3. Standard library or a platform feature? Use it, unless the project has its own. A house component beats a native widget.
|
|
16
|
+
4. An installed dependency? Use it. Never add a dependency for a few lines.
|
|
17
|
+
5. Can it be one line a reader gets at a glance? One line.
|
|
18
|
+
6. Otherwise: the minimum code that works.
|
|
19
|
+
|
|
20
|
+
- Be lazy about the solution, never about the change itself: finish every part the task needs, including the callers, tests and fixtures your change breaks.
|
|
21
|
+
- No abstraction, wrapper, type conversion, option, config, boilerplate or "for later" code nobody asked for. Keep values in the form the platform already gives you. Deletion beats addition. Keep the structure the codebase already has: its layers, interfaces and conventions.
|
|
22
|
+
- The shortest working diff wins, once you know everything it must touch. A one-liner that needs decoding is not short.
|
|
23
|
+
- Comment only the why the code cannot show, in one line.
|
|
24
|
+
- Bug fix: before you edit, grep every caller of the function you touch, then fix the root cause once in the shared code.
|
|
25
|
+
- Code you move or merge keeps its error handling and validation.
|
|
26
|
+
- Between options of equal size, take the one that is correct on edge cases.
|
|
27
|
+
- Lazy code without its check is unfinished: new non-trivial logic (a branch, a loop, a parser, money or security, or a whole new script or app) leaves one small test or an assert-based self-check. Trivial changes need none.
|
|
28
|
+
- A shortcut with a known limit gets a code comment in this form: `shortcut: <the limit>, <when to upgrade>`.
|
|
29
|
+
|
|
30
|
+
Never cut: validation at trust boundaries, error handling that prevents data loss, security, accessibility, the calibration real hardware needs, anything the user asked for.
|
package/AGENTS.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!-- chill-agent:start -->
|
|
2
|
+
## ChillAgent Anti-Slop 2.0 & Pragmatic Senior Dev Standard (Ponytail + Anti-Slop)
|
|
3
|
+
Active Mode: `full` (Options: lite | full | ultra). Switch anytime with `/chill <mode>`.
|
|
4
|
+
|
|
5
|
+
Before generating or editing any code, interface, copy, or architecture in this workspace, follow these active skills:
|
|
6
|
+
- `skills/chill-slop/SKILL.md`
|
|
7
|
+
- `skills/chill-slop-code/SKILL.md`
|
|
8
|
+
- `skills/chill-slop-ui/SKILL.md`
|
|
9
|
+
- `skills/chill-slop-copywriting/SKILL.md`
|
|
10
|
+
- `skills/chill-slop-human/SKILL.md`
|
|
11
|
+
- `skills/chill-slop-mobile/SKILL.md`
|
|
12
|
+
|
|
13
|
+
### Core Operating Rules:
|
|
14
|
+
1. **The Smallest Complete Change (Ponytail):** Take the shortest path that solves the whole task: standard library > existing codebase helper > installed dependency > minimum new code. No premature abstractions or wrapper factories.
|
|
15
|
+
2. **Purpose Test (Anti-Slop):** Every visual decision, color accent, layout structure, and copy phrase must name a specific domain purpose. AI defaults (purple gradients, rounded-3xl cards, generic cards) are forbidden.
|
|
16
|
+
3. **Liveliness Bar:** Avoid sterile defaults by reading `DESIGN.md`. If `DESIGN.md` is missing, adhere strictly to Linear/Geist carbon monochromatic standards.
|
|
17
|
+
4. **Context Hygiene & Time-Travel:** Strip conversational fluff and emojis. When debugging stochastic agent loops, route upstream LLM calls through `chillrewind run` (deterministic SQLite WAL replay + SlopSieve circuit breaker).
|
|
18
|
+
5. **Delivery Disclosure:** End non-trivial deliveries with a 1-line disclosure: what was skipped and known limits/trade-offs.
|
|
19
|
+
<!-- chill-agent:end -->
|
package/__init__.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""Hermes plugin for Ponytail."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
DEFAULT_MODE = "full"
|
|
12
|
+
RUNTIME_MODES = {"off", "lite", "full", "ultra"}
|
|
13
|
+
SKILL_COMMANDS = {
|
|
14
|
+
"ponytail-review": "Review the current diff or provided target: bugs, security, load, missing tests, speed, and what to cut.",
|
|
15
|
+
"ponytail-audit": "Audit the whole repo: bugs, security, load, missing tests, speed, and what to cut.",
|
|
16
|
+
"ponytail-debt": "List every deliberate `shortcut:` comment and its upgrade path.",
|
|
17
|
+
"ponytail-gain": "Show the measured-impact scoreboard (less code, less cost, more speed).",
|
|
18
|
+
"ponytail-help": "Show the Ponytail command reference.",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
ROOT = Path(__file__).resolve().parent
|
|
22
|
+
SKILLS_DIR = ROOT / "skills"
|
|
23
|
+
PONYTAIL_SKILL = SKILLS_DIR / "ponytail" / "SKILL.md"
|
|
24
|
+
|
|
25
|
+
_current_mode = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _normalize_runtime_mode(mode: str | None) -> str | None:
|
|
29
|
+
if not isinstance(mode, str):
|
|
30
|
+
return None
|
|
31
|
+
mode = mode.strip().lower()
|
|
32
|
+
return mode if mode in RUNTIME_MODES else None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _config_dir() -> Path:
|
|
36
|
+
if os.environ.get("XDG_CONFIG_HOME"):
|
|
37
|
+
return Path(os.environ["XDG_CONFIG_HOME"]) / "ponytail"
|
|
38
|
+
if os.name == "nt":
|
|
39
|
+
return Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming")) / "ponytail"
|
|
40
|
+
return Path.home() / ".config" / "ponytail"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _default_mode() -> str:
|
|
44
|
+
env_mode = _normalize_runtime_mode(os.environ.get("PONYTAIL_DEFAULT_MODE"))
|
|
45
|
+
if env_mode:
|
|
46
|
+
return env_mode
|
|
47
|
+
try:
|
|
48
|
+
data = json.loads((_config_dir() / "config.json").read_text(encoding="utf-8-sig"))
|
|
49
|
+
file_mode = _normalize_runtime_mode(data.get("defaultMode"))
|
|
50
|
+
if file_mode:
|
|
51
|
+
return file_mode
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
return DEFAULT_MODE
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _strip_frontmatter(text: str) -> str:
|
|
58
|
+
return re.sub(r"^---[\s\S]*?---\s*", "", text or "", count=1)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _filter_skill_body_for_mode(body: str, mode: str) -> str:
|
|
62
|
+
effective = _normalize_runtime_mode(mode) or DEFAULT_MODE
|
|
63
|
+
lines = []
|
|
64
|
+
# str.splitlines() drops a trailing line terminator instead of yielding a
|
|
65
|
+
# trailing empty element, so a body ending in "\n" loses that newline on
|
|
66
|
+
# rejoin. re.split keeps it, matching the JS filter's split(/\r?\n/).
|
|
67
|
+
for line in re.split(r"\r?\n", _strip_frontmatter(body)):
|
|
68
|
+
table_label = re.match(r"^\|\s*\*\*(.+?)\*\*\s*\|", line)
|
|
69
|
+
if table_label:
|
|
70
|
+
label_mode = _normalize_runtime_mode(table_label.group(1))
|
|
71
|
+
if label_mode and label_mode != effective:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
# Require a quoted value: every worked example is `- lite: "..."`. Without
|
|
75
|
+
# this, an ordinary rule bullet that happens to start with a mode word
|
|
76
|
+
# (e.g. "- Full: ...") is silently dropped in every other mode — it looks
|
|
77
|
+
# like a worked example but is really prose meant to survive verbatim.
|
|
78
|
+
# Mirrors the fix in hooks/ponytail-instructions.js (#571).
|
|
79
|
+
example_label = re.match(r'^-\s*([^:]+):\s*"', line)
|
|
80
|
+
if example_label:
|
|
81
|
+
label_mode = _normalize_runtime_mode(example_label.group(1))
|
|
82
|
+
if label_mode and label_mode != effective:
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
lines.append(line)
|
|
86
|
+
return "\n".join(lines)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _fallback_instructions(mode: str) -> str:
|
|
90
|
+
return (
|
|
91
|
+
f"PONYTAIL MODE ACTIVE — level: {mode}\n\n"
|
|
92
|
+
"You are a lazy senior developer. The best code is the code never written. "
|
|
93
|
+
"Before you write, list every place your change must reach (callers, tests, "
|
|
94
|
+
"fixtures) and what it could break for users. First rung that works: skip what "
|
|
95
|
+
"is not needed, reuse what the codebase has, stdlib or platform, installed "
|
|
96
|
+
"dependency, one readable line, then minimum code. No unrequested abstractions, "
|
|
97
|
+
"wrappers, options or boilerplate. Never cut trust-boundary validation, "
|
|
98
|
+
"data-loss handling, security, accessibility or anything asked for."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def build_injected_context(mode: str | None = None) -> str:
|
|
103
|
+
"""Return the mode-filtered Ponytail context injected before LLM turns."""
|
|
104
|
+
effective = _normalize_runtime_mode(mode) or _default_mode()
|
|
105
|
+
if effective == "off":
|
|
106
|
+
return ""
|
|
107
|
+
try:
|
|
108
|
+
body = PONYTAIL_SKILL.read_text(encoding="utf-8")
|
|
109
|
+
return f"PONYTAIL MODE ACTIVE — level: {effective}\n\n{_filter_skill_body_for_mode(body, effective)}"
|
|
110
|
+
except OSError:
|
|
111
|
+
return _fallback_instructions(effective)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _pre_llm_call(
|
|
115
|
+
session_id: str = "", conversation_history: Any = None, **_: Any
|
|
116
|
+
) -> dict[str, str] | None:
|
|
117
|
+
mode = _current_mode or _default_mode()
|
|
118
|
+
# Off is injected too: the ruleset from an earlier turn is replayed every turn,
|
|
119
|
+
# and the /ponytail off reply goes to the user, never to the model.
|
|
120
|
+
context = build_injected_context(mode) or "PONYTAIL MODE OFF"
|
|
121
|
+
|
|
122
|
+
# Hermes persists hook context in api_content for cache-stable replay.
|
|
123
|
+
# Re-inject only when the active mode changed or compaction removed it.
|
|
124
|
+
for message in reversed(conversation_history or []):
|
|
125
|
+
if not isinstance(message, dict):
|
|
126
|
+
continue
|
|
127
|
+
api_content = message.get("api_content")
|
|
128
|
+
if not isinstance(api_content, str):
|
|
129
|
+
continue
|
|
130
|
+
matches = re.findall(r"PONYTAIL MODE (?:ACTIVE — level: ([a-z]+)|OFF)", api_content)
|
|
131
|
+
if matches:
|
|
132
|
+
return None if (matches[-1] or "off") == mode else {"context": context}
|
|
133
|
+
|
|
134
|
+
# Nothing persisted, so off has nothing to switch off.
|
|
135
|
+
return None if mode == "off" else {"context": context}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _skill_prompt(command: str, args: str = "") -> str:
|
|
139
|
+
tail = args.strip()
|
|
140
|
+
target = f"\n\nUser arguments: {tail}" if tail else ""
|
|
141
|
+
return (
|
|
142
|
+
f"Load and follow the Hermes plugin skill `ponytail:{command}`. "
|
|
143
|
+
f"{SKILL_COMMANDS[command]}{target}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _slash_access_denied(event: Any, gateway: Any, command: str) -> bool:
|
|
148
|
+
if gateway is None or event is None:
|
|
149
|
+
return False
|
|
150
|
+
checker = getattr(gateway, "_check_slash_access", None)
|
|
151
|
+
source = getattr(event, "source", None)
|
|
152
|
+
if checker is None or source is None:
|
|
153
|
+
return False
|
|
154
|
+
try:
|
|
155
|
+
return checker(source, command) is not None
|
|
156
|
+
except Exception:
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def rewrite_gateway_command(event: Any = None, gateway: Any = None, **_: Any) -> dict[str, str] | None:
|
|
161
|
+
"""Rewrite authorized gateway /ponytail-* commands into normal agent prompts."""
|
|
162
|
+
text = str(getattr(event, "text", "") or "").strip()
|
|
163
|
+
if not text.startswith("/"):
|
|
164
|
+
return None
|
|
165
|
+
head, _, rest = text[1:].partition(" ")
|
|
166
|
+
command = head.replace("_", "-").lower()
|
|
167
|
+
if command not in SKILL_COMMANDS:
|
|
168
|
+
return None
|
|
169
|
+
if _slash_access_denied(event, gateway, command):
|
|
170
|
+
return None
|
|
171
|
+
return {"action": "rewrite", "text": _skill_prompt(command, rest)}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _handle_mode_command(raw_args: str) -> str:
|
|
175
|
+
global _current_mode
|
|
176
|
+
arg = (raw_args or "").strip().lower()
|
|
177
|
+
if not arg:
|
|
178
|
+
# Bare /ponytail switches ponytail on, or reports the level when it already is (#639).
|
|
179
|
+
mode = _current_mode or _default_mode()
|
|
180
|
+
if mode != "off":
|
|
181
|
+
return f"Ponytail mode: {mode}. Use `/ponytail lite|full|ultra|off`."
|
|
182
|
+
_current_mode = "full" if _default_mode() == "off" else _default_mode()
|
|
183
|
+
return f"Ponytail mode set to {_current_mode}."
|
|
184
|
+
mode = _normalize_runtime_mode(arg)
|
|
185
|
+
if not mode:
|
|
186
|
+
return "Usage: /ponytail [lite|full|ultra|off]"
|
|
187
|
+
_current_mode = mode
|
|
188
|
+
return f"Ponytail mode set to {mode}."
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _make_skill_command_handler(ctx: Any, command: str) -> Callable[[str], str]:
|
|
192
|
+
def handler(raw_args: str) -> str:
|
|
193
|
+
prompt = _skill_prompt(command, raw_args or "")
|
|
194
|
+
injected = False
|
|
195
|
+
try:
|
|
196
|
+
injected = bool(ctx.inject_message(prompt))
|
|
197
|
+
except Exception:
|
|
198
|
+
injected = False
|
|
199
|
+
if injected:
|
|
200
|
+
return f"Queued `{command}` for the agent."
|
|
201
|
+
return prompt
|
|
202
|
+
|
|
203
|
+
return handler
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def register(ctx: Any) -> None:
|
|
207
|
+
"""Register Ponytail hooks, skills, and slash commands with Hermes."""
|
|
208
|
+
for child in sorted(SKILLS_DIR.iterdir() if SKILLS_DIR.exists() else []):
|
|
209
|
+
skill_md = child / "SKILL.md"
|
|
210
|
+
if child.is_dir() and skill_md.exists():
|
|
211
|
+
ctx.register_skill(child.name, skill_md)
|
|
212
|
+
|
|
213
|
+
ctx.register_hook("pre_llm_call", _pre_llm_call)
|
|
214
|
+
ctx.register_hook("pre_gateway_dispatch", rewrite_gateway_command)
|
|
215
|
+
|
|
216
|
+
ctx.register_command(
|
|
217
|
+
"ponytail",
|
|
218
|
+
_handle_mode_command,
|
|
219
|
+
description="Set Ponytail lazy senior dev mode: lite, full, ultra, or off.",
|
|
220
|
+
args_hint="[lite|full|ultra|off]",
|
|
221
|
+
)
|
|
222
|
+
for command, description in SKILL_COMMANDS.items():
|
|
223
|
+
ctx.register_command(
|
|
224
|
+
command,
|
|
225
|
+
_make_skill_command_handler(ctx, command),
|
|
226
|
+
description=description,
|
|
227
|
+
args_hint="[target or notes]",
|
|
228
|
+
)
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
description = "Quality audit of the whole repo: bugs, security, load, tests, speed, bloat"
|
|
2
|
+
prompt = "Audit the whole repository (or the folder the user names) like the senior developer who just inherited it. Map first: README, deploy and build config, dependencies, entry points, tests; find the expected load (one person or many users and processes); trace the main flows and read fully the paths with user input, money, auth, data writes, background jobs and shared state. Look for, in this order: bugs, risk (security holes, data loss), scale (races, the same work done by every process, memory that only grows, a query per item, O(n^2)), risky logic without a test, slow code (big = problem, small = suggestion), lean (dead code, duplicate helpers, stdlib or native instead of custom code or a dependency, abstractions with one implementation, near-copies to merge, a function doing several unrelated jobs to split, never split by line count). Before calling code unused, grep the whole tree including tests, fixtures, config and string references. Every finding needs a concrete case. A shortcut marked with a shortcut: (or older ponytail:) comment that names its limit is a decision, not a finding. Propose the smallest fix, prefer deleting code, never add layers. Very simple English. Start with 'What this repo does:' and the load you assumed. Then at most 20 numbered findings grouped Must fix / Should fix / Nice to have, most important first; each: a bold title with file:lines, then four bullets: What this is, Problem, Fix, If we skip it. End with 'Verdict:', 'Lean: -N lines, -M dependencies possible.' and 'Not checked:' for the parts you did not read. Change no code."
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
description = "Harvest shortcut: comments into a tracked debt ledger"
|
|
2
|
+
prompt = "Harvest every `shortcut:` comment (and older `ponytail:` ones) in this repository into a debt ledger so deferrals do not rot into 'later means never'. Grep the whole tree for comment markers (git grep --no-index --exclude-standard -nE '(#|//|/[*]) ?(shortcut|ponytail):', skipping .git and everything .gitignore ignores; if the user names their own marker word, grep for that instead; skip hits that are not a deferral, like a keyboard-shortcut note). One row per marker, grouped by file: <file>:<line> — <what was simplified>. ceiling: <the limit named in the comment>. upgrade: <the trigger to revisit>. Tag any marker that names no upgrade path or trigger as no-trigger, those rot silently. End with the count of markers and how many lack a trigger. If none: 'No shortcut debt. Clean ledger.' Report only, change nothing."
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
description = "Show ponytail's measured impact scoreboard (less code, cost, time)"
|
|
2
|
+
prompt = "Show the ponytail gain scoreboard. One shot, change nothing: do not switch mode, write flag files, or persist anything. Render the published agentic benchmark of Ponytail 5 (39 tasks on a real FastAPI + React repo plus bug fixes, security and privacy cases and small apps, Opus 5.5, 5 runs each, against the same agent without the skill; geometric mean of per-task medians; source benchmarks/results/2026-10-07-agentic.md and the README) as plain ASCII bars, ponytail as a share of the no-skill baseline (100%): Lines of code 47% (down 53%); Output tokens 55% (down 45%); Cost 74% (down 26%); Time 59% (down 41%); Hidden checks passed 97% (no-skill 96%; 18 of the tasks have hidden checks for correctness and safety); Tests where the logic needs one 98% (no-skill 68%). The bar length shows the share, the label carries the exact figure. These are benchmark averages, not this repo. NEVER print a per-repo savings number: the unbuilt version was never written, so there is no real baseline to subtract from in a live repo. For real per-repo figures, point to /ponytail-debt (the counted shortcut ledger) and /ponytail-audit (what is still cuttable). Report only."
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
description = "Quick reference for ponytail levels, skills, and commands"
|
|
2
|
+
prompt = "Show the ponytail quick reference. One shot, change nothing: do not switch mode, write flag files, or persist anything. Levels: /ponytail lite (build what's asked, name the lazier alternative in one line), /ponytail (full, the default ladder: YAGNI then stdlib then native then one line then minimum), /ponytail ultra (deletion before addition, challenges the requirement before building). Commands: /ponytail-review (over-engineering review of the current changes), /ponytail-audit (whole-repo over-engineering audit), /ponytail-debt (harvest shortcut: comments into a tracked ledger), /ponytail-gain (measured-impact scoreboard from the benchmark), /ponytail-help (this card). Deactivate with 'stop ponytail', 'normal mode', or /ponytail off; resume anytime with /ponytail. Default mode is full; change it with the PONYTAIL_DEFAULT_MODE environment variable (off|lite|full|ultra) or a config file at ~/.config/ponytail/config.json (Windows: %APPDATA%\\ponytail\\config.json) with {\"defaultMode\": \"lite\"}. Resolution order: env var, then config file, then full."
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
description = "Quality review of the current changes: bugs, security, load, tests, speed, bloat"
|
|
2
|
+
prompt = "Review the current code changes like the senior developer who will be paged when they break. Review what the user names (uncommitted or staged changes, a branch, a PR link, files); nothing named: the uncommitted changes, or the last commit if there are none. First read the code the change touches: callers of every changed function, the functions it calls, the tests, the README (find the expected load: one person or many users and processes). A change can break code it does not touch. Look for, in this order: bugs (wrong result, crash, missed edge case, a caller broken by the change, a fix in one caller while the shared function stays broken), risk (security holes, data loss), scale (check-then-write races, the same work done by every process, memory that only grows, a query per item, O(n^2) on big input), risky logic without a test, slow code (big = problem, small = suggestion), lean (dead code, a helper the repo already has, stdlib or native instead of custom code or a new dependency, abstractions with one implementation, near-copies to merge, a function doing several unrelated jobs to split, never split by line count). Every finding needs a concrete case; re-read the lines to confirm it. A shortcut marked with a shortcut: (or older ponytail:) comment that names its limit is a decision, not a finding. Propose the smallest fix, prefer deleting code, never add layers. No style taste, no vague worries. Very simple English, explain technical terms. Start with 'What this change does:' in two or three sentences and the load you assumed. Then numbered findings grouped Must fix / Should fix / Nice to have; each finding: a bold title with file:lines, then four bullets: What this is, Problem, Fix, If we skip it (one or two short sentences each). End with 'Verdict:', 'Lean: -N lines possible.' when lean findings exist, and 'Not checked:' if something mattered and you could not check it. Nothing found: 'Looks good. Ship.' Change no code."
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
description = "Switch ponytail intensity level (lite/full/ultra/off)"
|
|
2
|
+
prompt = "Switch to ponytail {{args}} mode. With no level, keep the current level, or switch on at the default level if ponytail was off. Lazy senior dev mode, before any code: does it need to exist at all (YAGNI)? Does the standard library do it? A native platform feature? Can it be one line? Build the minimum that works. No unrequested abstractions, no avoidable dependencies, no boilerplate. Mark deliberate simplifications that cut a real corner with a known ceiling using a shortcut: comment that names the ceiling and upgrade path."
|