Merge PR #7: Codex support (avidspartan1)
Adds the Codex adapter: marketplace.json, .codex-plugin/plugin.json, declarative hooks.json with Windows command variants, and a shared hooks/ponytail-runtime.js that switches state path and output format between Claude and Codex (gated on PLUGIN_DATA, so the Claude path is unchanged). Bumps plugin version to 4.1.0 and ships tests/hooks.test.js. README adapters block resolved to keep the Codex @ponytail invocation note together with Kiro (#6) and the drift-check Development section (#3). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "ponytail",
|
||||||
|
"interface": {
|
||||||
|
"displayName": "Ponytail"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "ponytail",
|
||||||
|
"source": {
|
||||||
|
"source": "url",
|
||||||
|
"url": "https://github.com/DietrichGebert/ponytail.git",
|
||||||
|
"ref": "main"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"installation": "AVAILABLE",
|
||||||
|
"authentication": "ON_INSTALL"
|
||||||
|
},
|
||||||
|
"category": "Productivity"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ponytail",
|
"name": "ponytail",
|
||||||
"version": "4.0.0",
|
"version": "4.1.0",
|
||||||
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Dietrich Gebert",
|
"name": "Dietrich Gebert",
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "ponytail",
|
||||||
|
"version": "4.1.0",
|
||||||
|
"description": "Lazy senior dev mode. Forces the simplest, shortest solution that actually works: YAGNI, stdlib first, no unrequested abstractions.",
|
||||||
|
"author": {
|
||||||
|
"name": "Dietrich Gebert",
|
||||||
|
"url": "https://github.com/DietrichGebert"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/DietrichGebert/ponytail",
|
||||||
|
"repository": "https://github.com/DietrichGebert/ponytail",
|
||||||
|
"license": "MIT",
|
||||||
|
"keywords": ["yagni", "minimalism", "code-review", "productivity"],
|
||||||
|
"skills": "./skills/",
|
||||||
|
"interface": {
|
||||||
|
"displayName": "Ponytail",
|
||||||
|
"shortDescription": "Lazy senior developer mode",
|
||||||
|
"longDescription": "Prefer YAGNI, the standard library, native platform features, and the smallest correct implementation.",
|
||||||
|
"developerName": "Dietrich Gebert",
|
||||||
|
"category": "Productivity",
|
||||||
|
"capabilities": ["Instructions", "Lifecycle hooks"],
|
||||||
|
"websiteURL": "https://github.com/DietrichGebert/ponytail",
|
||||||
|
"defaultPrompt": [
|
||||||
|
"Use Ponytail mode for this task.",
|
||||||
|
"Review this diff for over-engineering.",
|
||||||
|
"Find the smallest correct implementation."
|
||||||
|
],
|
||||||
|
"brandColor": "#111111",
|
||||||
|
"composerIcon": "./assets/logo.png",
|
||||||
|
"logo": "./assets/logo.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,15 +56,30 @@ Lazy, not negligent: trust-boundary validation, data-loss handling, security, an
|
|||||||
|
|
||||||
The most effort ponytail will ever ask of you:
|
The most effort ponytail will ever ask of you:
|
||||||
|
|
||||||
|
### Claude Code
|
||||||
|
|
||||||
```
|
```
|
||||||
/plugin marketplace add DietrichGebert/ponytail
|
/plugin marketplace add DietrichGebert/ponytail
|
||||||
/plugin install ponytail@ponytail
|
/plugin install ponytail@ponytail
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Codex
|
||||||
|
|
||||||
|
```bash
|
||||||
|
codex plugin marketplace add DietrichGebert/ponytail
|
||||||
|
codex
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `/plugins`, select the Ponytail marketplace, and install Ponytail. Then
|
||||||
|
open `/hooks`, review and trust its two lifecycle hooks, and start a new thread.
|
||||||
|
|
||||||
That was it. He'd be proud. He won't say it.
|
That was it. He'd be proud. He won't say it.
|
||||||
|
|
||||||
Active every session. `/ponytail-review` finds what to delete in your diff. `/ponytail ultra` exists for when the codebase has wronged you personally. `/ponytail-help` explains the rest.
|
Active every session. `/ponytail-review` finds what to delete in your diff. `/ponytail ultra` exists for when the codebase has wronged you personally. `/ponytail-help` explains the rest.
|
||||||
|
|
||||||
|
In Codex, invoke the skills as `@ponytail`, `@ponytail-review`, and
|
||||||
|
`@ponytail-help`. Startup and mode-change text shows the current mode.
|
||||||
|
|
||||||
Cursor, Windsurf, Cline, Copilot, Aider, Kiro: copy the matching rules file from this repo ([`.cursor/rules/`](.cursor/rules/), [`.windsurf/rules/`](.windsurf/rules/), [`.clinerules/`](.clinerules/), [`.github/copilot-instructions.md`](.github/copilot-instructions.md), [`AGENTS.md`](AGENTS.md), [`.kiro/steering/`](.kiro/steering/)).
|
Cursor, Windsurf, Cline, Copilot, Aider, Kiro: copy the matching rules file from this repo ([`.cursor/rules/`](.cursor/rules/), [`.windsurf/rules/`](.windsurf/rules/), [`.clinerules/`](.clinerules/), [`.github/copilot-instructions.md`](.github/copilot-instructions.md), [`AGENTS.md`](AGENTS.md), [`.kiro/steering/`](.kiro/steering/)).
|
||||||
|
|
||||||
Kiro: copy `.kiro/steering/ponytail.md` to `~/.kiro/steering/` (global) or `.kiro/steering/` in your project.
|
Kiro: copy `.kiro/steering/ponytail.md` to `~/.kiro/steering/` (global) or `.kiro/steering/` in your project.
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"SessionStart": [
|
||||||
|
{
|
||||||
|
"matcher": "startup|resume|clear|compact",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node \"${PLUGIN_ROOT}/hooks/ponytail-activate.js\"",
|
||||||
|
"commandWindows": "node \"%PLUGIN_ROOT%\\hooks\\ponytail-activate.js\"",
|
||||||
|
"timeout": 5,
|
||||||
|
"statusMessage": "Loading ponytail mode..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"UserPromptSubmit": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node \"${PLUGIN_ROOT}/hooks/ponytail-mode-tracker.js\"",
|
||||||
|
"commandWindows": "node \"%PLUGIN_ROOT%\\hooks\\ponytail-mode-tracker.js\"",
|
||||||
|
"timeout": 5,
|
||||||
|
"statusMessage": "Tracking ponytail mode..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,24 +10,28 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const { getDefaultMode } = require('./ponytail-config');
|
const { getDefaultMode } = require('./ponytail-config');
|
||||||
|
const {
|
||||||
|
clearMode,
|
||||||
|
isCodex,
|
||||||
|
setMode,
|
||||||
|
writeHookOutput,
|
||||||
|
} = require('./ponytail-runtime');
|
||||||
|
|
||||||
const claudeDir = path.join(os.homedir(), '.claude');
|
const claudeDir = path.join(os.homedir(), '.claude');
|
||||||
const flagPath = path.join(claudeDir, '.ponytail-active');
|
|
||||||
const settingsPath = path.join(claudeDir, 'settings.json');
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
||||||
|
|
||||||
const mode = getDefaultMode();
|
const mode = getDefaultMode();
|
||||||
|
|
||||||
// "off" mode — skip activation entirely, don't write flag or emit rules
|
// "off" mode — skip activation entirely, don't write flag or emit rules
|
||||||
if (mode === 'off') {
|
if (mode === 'off') {
|
||||||
try { fs.unlinkSync(flagPath); } catch (e) {}
|
clearMode();
|
||||||
process.stdout.write('OK');
|
writeHookOutput('SessionStart', 'off', isCodex ? '' : 'OK');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Write flag file
|
// 1. Write flag file
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(path.dirname(flagPath), { recursive: true });
|
setMode(mode);
|
||||||
fs.writeFileSync(flagPath, mode);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silent fail -- flag is best-effort, don't block the hook
|
// Silent fail -- flag is best-effort, don't block the hook
|
||||||
}
|
}
|
||||||
@@ -45,7 +49,11 @@ try {
|
|||||||
const INDEPENDENT_MODES = new Set(['review']);
|
const INDEPENDENT_MODES = new Set(['review']);
|
||||||
|
|
||||||
if (INDEPENDENT_MODES.has(mode)) {
|
if (INDEPENDENT_MODES.has(mode)) {
|
||||||
process.stdout.write('PONYTAIL MODE ACTIVE — level: ' + mode + '. Behavior defined by /ponytail-' + mode + ' skill.');
|
writeHookOutput(
|
||||||
|
'SessionStart',
|
||||||
|
mode,
|
||||||
|
'PONYTAIL MODE ACTIVE — level: ' + mode + '. Behavior defined by /ponytail-' + mode + ' skill.',
|
||||||
|
);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +132,7 @@ if (skillContent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Detect missing statusline config — nudge Claude to help set it up
|
// 3. Detect missing statusline config — nudge Claude to help set it up
|
||||||
try {
|
if (!isCodex) try {
|
||||||
let hasStatusline = false;
|
let hasStatusline = false;
|
||||||
if (fs.existsSync(settingsPath)) {
|
if (fs.existsSync(settingsPath)) {
|
||||||
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
||||||
@@ -153,4 +161,4 @@ try {
|
|||||||
// Silent fail — don't block session start over statusline detection
|
// Silent fail — don't block session start over statusline detection
|
||||||
}
|
}
|
||||||
|
|
||||||
process.stdout.write(output);
|
writeHookOutput('SessionStart', mode, output);
|
||||||
|
|||||||
@@ -2,12 +2,8 @@
|
|||||||
// ponytail — UserPromptSubmit hook to track which ponytail mode is active
|
// ponytail — UserPromptSubmit hook to track which ponytail mode is active
|
||||||
// Inspects user input for /ponytail commands and writes mode to flag file
|
// Inspects user input for /ponytail commands and writes mode to flag file
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const os = require('os');
|
|
||||||
const { getDefaultMode } = require('./ponytail-config');
|
const { getDefaultMode } = require('./ponytail-config');
|
||||||
|
const { clearMode, setMode, writeHookOutput } = require('./ponytail-runtime');
|
||||||
const flagPath = path.join(os.homedir(), '.claude', '.ponytail-active');
|
|
||||||
|
|
||||||
let input = '';
|
let input = '';
|
||||||
process.stdin.on('data', chunk => { input += chunk; });
|
process.stdin.on('data', chunk => { input += chunk; });
|
||||||
@@ -18,9 +14,9 @@ process.stdin.on('end', () => {
|
|||||||
const prompt = (data.prompt || '').trim().toLowerCase();
|
const prompt = (data.prompt || '').trim().toLowerCase();
|
||||||
|
|
||||||
// Match /ponytail commands
|
// Match /ponytail commands
|
||||||
if (prompt.startsWith('/ponytail')) {
|
if (/^[/@$]ponytail/.test(prompt)) {
|
||||||
const parts = prompt.split(/\s+/);
|
const parts = prompt.split(/\s+/);
|
||||||
const cmd = parts[0]; // /ponytail, /ponytail-review, /ponytail:ponytail, etc.
|
const cmd = parts[0].replace(/^[@$]/, '/');
|
||||||
const arg = parts[1] || '';
|
const arg = parts[1] || '';
|
||||||
|
|
||||||
let mode = null;
|
let mode = null;
|
||||||
@@ -36,16 +32,22 @@ process.stdin.on('end', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mode && mode !== 'off') {
|
if (mode && mode !== 'off') {
|
||||||
fs.mkdirSync(path.dirname(flagPath), { recursive: true });
|
setMode(mode);
|
||||||
fs.writeFileSync(flagPath, mode);
|
writeHookOutput(
|
||||||
|
'UserPromptSubmit',
|
||||||
|
mode,
|
||||||
|
'PONYTAIL MODE CHANGED — level: ' + mode,
|
||||||
|
);
|
||||||
} else if (mode === 'off') {
|
} else if (mode === 'off') {
|
||||||
try { fs.unlinkSync(flagPath); } catch (e) {}
|
clearMode();
|
||||||
|
writeHookOutput('UserPromptSubmit', 'off', 'PONYTAIL MODE OFF');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect deactivation
|
// Detect deactivation
|
||||||
if (/\b(stop ponytail|normal mode)\b/i.test(prompt)) {
|
if (/\b(stop ponytail|normal mode)\b/i.test(prompt)) {
|
||||||
try { fs.unlinkSync(flagPath); } catch (e) {}
|
clearMode();
|
||||||
|
writeHookOutput('UserPromptSubmit', 'off', 'PONYTAIL MODE OFF');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silent fail
|
// Silent fail
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
|
||||||
|
const isCodex = Boolean(process.env.PLUGIN_DATA);
|
||||||
|
const statePath = isCodex
|
||||||
|
? path.join(process.env.PLUGIN_DATA, '.ponytail-active')
|
||||||
|
: path.join(os.homedir(), '.claude', '.ponytail-active');
|
||||||
|
|
||||||
|
function setMode(mode) {
|
||||||
|
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
||||||
|
fs.writeFileSync(statePath, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMode() {
|
||||||
|
try { fs.unlinkSync(statePath); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeHookOutput(event, mode, context = '') {
|
||||||
|
if (!isCodex) {
|
||||||
|
process.stdout.write(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const output = { systemMessage: `PONYTAIL:${mode.toUpperCase()}` };
|
||||||
|
if (context) {
|
||||||
|
output.hookSpecificOutput = {
|
||||||
|
hookEventName: event,
|
||||||
|
additionalContext: context,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
process.stdout.write(JSON.stringify(output));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
clearMode,
|
||||||
|
isCodex,
|
||||||
|
setMode,
|
||||||
|
writeHookOutput,
|
||||||
|
};
|
||||||
@@ -29,6 +29,9 @@ Level sticks until changed or session end.
|
|||||||
| **ponytail-review** | `/ponytail-review` | Over-engineering review: `L42: yagni: factory, one product. Inline.` |
|
| **ponytail-review** | `/ponytail-review` | Over-engineering review: `L42: yagni: factory, one product. Inline.` |
|
||||||
| **ponytail-help** | `/ponytail-help` | This card. |
|
| **ponytail-help** | `/ponytail-help` | This card. |
|
||||||
|
|
||||||
|
Codex uses `@ponytail`, `@ponytail-review`, and `@ponytail-help`; Claude Code
|
||||||
|
uses the slash-command forms above.
|
||||||
|
|
||||||
## Deactivate
|
## Deactivate
|
||||||
|
|
||||||
Say "stop ponytail" or "normal mode". Resume anytime with `/ponytail`.
|
Say "stop ponytail" or "normal mode". Resume anytime with `/ponytail`.
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const path = require('path');
|
||||||
|
const { spawnSync } = require('child_process');
|
||||||
|
|
||||||
|
const root = path.join(__dirname, '..');
|
||||||
|
|
||||||
|
function run(script, env, input = '') {
|
||||||
|
return spawnSync(process.execPath, [path.join(root, 'hooks', script)], {
|
||||||
|
env: { ...process.env, ...env },
|
||||||
|
input,
|
||||||
|
encoding: 'utf8',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ponytail-hooks-'));
|
||||||
|
const home = path.join(temp, 'home');
|
||||||
|
const pluginData = path.join(temp, 'plugin-data');
|
||||||
|
fs.mkdirSync(home, { recursive: true });
|
||||||
|
|
||||||
|
const codexEnv = {
|
||||||
|
HOME: home,
|
||||||
|
PLUGIN_DATA: pluginData,
|
||||||
|
PONYTAIL_DEFAULT_MODE: 'ultra',
|
||||||
|
};
|
||||||
|
const codexState = path.join(pluginData, '.ponytail-active');
|
||||||
|
|
||||||
|
let result = run('ponytail-activate.js', codexEnv);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(fs.readFileSync(codexState, 'utf8'), 'ultra');
|
||||||
|
let output = JSON.parse(result.stdout);
|
||||||
|
assert.equal(output.systemMessage, 'PONYTAIL:ULTRA');
|
||||||
|
assert.match(
|
||||||
|
output.hookSpecificOutput.additionalContext,
|
||||||
|
/PONYTAIL MODE ACTIVE — level: ultra/,
|
||||||
|
);
|
||||||
|
|
||||||
|
result = run(
|
||||||
|
'ponytail-mode-tracker.js',
|
||||||
|
codexEnv,
|
||||||
|
JSON.stringify({ prompt: '@ponytail lite' }),
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(fs.readFileSync(codexState, 'utf8'), 'lite');
|
||||||
|
output = JSON.parse(result.stdout);
|
||||||
|
assert.equal(output.systemMessage, 'PONYTAIL:LITE');
|
||||||
|
|
||||||
|
result = run(
|
||||||
|
'ponytail-mode-tracker.js',
|
||||||
|
codexEnv,
|
||||||
|
JSON.stringify({ prompt: 'normal mode' }),
|
||||||
|
);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(fs.existsSync(codexState), false);
|
||||||
|
output = JSON.parse(result.stdout);
|
||||||
|
assert.equal(output.systemMessage, 'PONYTAIL:OFF');
|
||||||
|
|
||||||
|
const claudeEnv = {
|
||||||
|
HOME: home,
|
||||||
|
PONYTAIL_DEFAULT_MODE: 'full',
|
||||||
|
};
|
||||||
|
delete claudeEnv.PLUGIN_DATA;
|
||||||
|
|
||||||
|
result = run('ponytail-activate.js', claudeEnv);
|
||||||
|
assert.equal(result.status, 0, result.stderr);
|
||||||
|
assert.equal(
|
||||||
|
fs.readFileSync(path.join(home, '.claude', '.ponytail-active'), 'utf8'),
|
||||||
|
'full',
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.rmSync(temp, { recursive: true, force: true });
|
||||||
|
console.log('hook compatibility checks passed');
|
||||||
Reference in New Issue
Block a user