Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .openclaw/skills/ponytail-help/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ Say "stop ponytail" or "normal mode". Resume anytime with `/ponytail`.

## Configure Default Mode

Default mode = `full`, auto-active every session. Change it:
Default mode = `full`, auto-active every session. `/ponytail off` lasts only for
the current session — the next one starts at the default again. Change it:

**Command**:
```
/ponytail default lite
```

**Environment variable** (highest priority):
```bash
Expand Down
3 changes: 2 additions & 1 deletion .openclaw/skills/ponytail/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ code is the code never written.

ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if
unsure. Off only: "stop ponytail" / "normal mode". Default: **full**.
Switch: `/ponytail lite|full|ultra`.
Switch: `/ponytail lite|full|ultra|off`. Bare `/ponytail` reports the level.
`/ponytail default <mode>` persists it to new sessions.

## The ladder

Expand Down
3 changes: 2 additions & 1 deletion README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,8 @@ Qué archivos corresponden a qué agente: [Portabilidad de agentes](docs/agent-p

| Comando | Qué hace |
|---------|----------|
| `/ponytail [lite \| full \| ultra \| off]` | Cambia la intensidad, o apágalo. Sin argumento, reporta el nivel actual. |
| `/ponytail [lite \| full \| ultra \| off]` | Cambia la intensidad, o apágalo. Sin argumento, reporta el nivel actual. Dura solo la sesión: una sesión nueva arranca en el valor por defecto. |
| `/ponytail default [lite \| full \| ultra \| off]` | Guarda el valor por defecto, para que las sesiones nuevas arranquen ahí. |
| `/ponytail-review` | Revisa el diff actual en busca de sobre-ingeniería y devuelve una lista de qué eliminar. |
| `/ponytail-audit` | Audita el repo completo en busca de sobre-ingeniería, no solo el diff. |
| `/ponytail-debt` | Recolecta los atajos marcados con `ponytail:` que dejaste pendientes en un registro, para que "después" no se convierta en "nunca". |
Expand Down
3 changes: 2 additions & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,8 @@ Codex 확장을 쓰는 VS Code는 이 저장소가 함께 싣는 `AGENTS.md`를

| 명령 | 하는 일 |
|---------|--------------|
| `/ponytail [lite \| full \| ultra \| off]` | 강도를 정하거나, 끈다. 인수가 없으면 지금 레벨을 알려 준다. |
| `/ponytail [lite \| full \| ultra \| off]` | 강도를 정하거나, 끈다. 인수가 없으면 지금 레벨을 알려 준다. 세션 한정이라, 새 세션은 기본값에서 시작한다. |
| `/ponytail default [lite \| full \| ultra \| off]` | 기본값을 저장해서, 새 세션이 그 레벨에서 시작하게 한다. |
| `/ponytail-review` | 지금 diff를 과잉 구현 관점에서 훑고, 삭제 목록을 돌려준다. |
| `/ponytail-audit` | diff만이 아니라 저장소 전체를 과잉 구현 관점에서 감사한다. |
| `/ponytail-debt` | 미뤄 둔 `ponytail:` 간소화들을 장부로 모아, "나중에"가 "영영"이 되지 않게 한다. |
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ These remove the plugin's own files. They leave behind a small amount of state p

| Command | What it does |
|---------|--------------|
| `/ponytail [lite \| full \| ultra \| off]` | Set the intensity, or turn it off. No argument reports the current level. |
| `/ponytail [lite \| full \| ultra \| off]` | Set the intensity, or turn it off. No argument reports the current level. Session-scoped: a new session starts at the default. |
| `/ponytail default [lite \| full \| ultra \| off]` | Persist the default, so new sessions start there. |
| `/ponytail-review` | Review the current diff for over-engineering, hands back a delete-list. |
| `/ponytail-audit` | Audit the whole repo for over-engineering, not just the diff. |
| `/ponytail-debt` | Harvest the `ponytail:` shortcuts you've deferred into a ledger, so "later" doesn't become "never". |
Expand Down
3 changes: 2 additions & 1 deletion hooks/ponytail-instructions.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ function getFallbackInstructions(mode) {
'You are a lazy senior developer. Lazy means efficient, not careless. The best code is the code never written.\n\n' +
'## Persistence\n\n' +
'ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if unsure. Off only: "stop ponytail" / "normal mode".\n\n' +
'Current level: **' + mode + '**. Switch: `/ponytail lite|full|ultra`.\n\n' +
'Current level: **' + mode + '**. Switch: `/ponytail lite|full|ultra|off`. ' +
'Bare `/ponytail` reports the level; `/ponytail default <mode>` persists it.\n\n' +
'## The ladder\n\n' +
'Before any code, stop at the first rung that holds (the ladder runs after you understand the problem, not instead of it — read the code it touches and trace the real flow first):\n' +
'1. Does this need to be built at all? (YAGNI)\n' +
Expand Down
31 changes: 28 additions & 3 deletions hooks/ponytail-mode-tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ const { getPonytailInstructions } = require('./ponytail-instructions');
let input = '';
let done = false;

// Name the default only when it differs from the live level, so the common case
// stays a three-word status line and the surprising case explains itself.
function describeDefault(mode) {
const fallback = getDefaultMode();
return fallback === mode ? '' : ' (default: ' + fallback + ')';
}

// Off is session-scoped: clearMode() only drops the flag, and the next
// SessionStart re-activates at the configured default. Say so here, or the
// user reads tomorrow's active session as ponytail ignoring them (#648).
function offMessage() {
const fallback = getDefaultMode();
return fallback === 'off'
? 'Ponytail off.'
: 'Ponytail off for this session — default is ' + fallback +
', so a new session starts there. `/ponytail default off` to persist.';
}

function finish() {
if (done) return;
done = true;
Expand Down Expand Up @@ -39,7 +57,12 @@ function finish() {
const dmode = parts[2];
if (dmode === 'off' || dmode === 'lite' || dmode === 'full' || dmode === 'ultra') {
writeDefaultMode(dmode);
writeHookOutput('UserPromptSubmit', dmode, 'PONYTAIL DEFAULT SET — new sessions start in ' + dmode + '.');
writeHookOutput(
'UserPromptSubmit',
dmode,
'PONYTAIL DEFAULT SET — new sessions start in ' + dmode + '.',
'Ponytail default set to ' + dmode + ' — new sessions start there.',
);
}
return; // don't fall through to the session-mode switch
}
Expand All @@ -60,6 +83,7 @@ function finish() {
'UserPromptSubmit',
mode,
'PONYTAIL MODE ACTIVE — level: ' + mode,
'Ponytail: ' + mode + describeDefault(mode),
);
} else if (mode && mode !== 'off') {
setMode(mode);
Expand All @@ -72,20 +96,21 @@ function finish() {
'UserPromptSubmit',
mode,
'PONYTAIL MODE CHANGED — level: ' + mode,
'Ponytail: ' + mode + describeDefault(mode),
);
}
} else if (mode === 'off') {
clearMode();
deactivated = true;
writeHookOutput('UserPromptSubmit', 'off', 'PONYTAIL MODE OFF');
writeHookOutput('UserPromptSubmit', 'off', 'PONYTAIL MODE OFF', offMessage());
}
}

// Detect deactivation
if (!modeSwitched && !deactivated && isDeactivationCommand(prompt)) {
clearMode();
deactivated = true;
writeHookOutput('UserPromptSubmit', 'off', 'PONYTAIL MODE OFF');
writeHookOutput('UserPromptSubmit', 'off', 'PONYTAIL MODE OFF', offMessage());
}

// Qoder has no SessionStart event, so UserPromptSubmit does double duty:
Expand Down
17 changes: 16 additions & 1 deletion hooks/ponytail-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ function readMode() {
}
}

function writeHookOutput(event, mode, context = '') {
// `userMessage`, when set, is text the user asked for and must actually see.
// Every other write is context for the model. The distinction matters because on
// native Claude a UserPromptSubmit hook's stdout becomes additionalContext —
// injected into the assistant's conversation, never shown — so a mode report
// written that way reaches everyone except the person who typed the command
// (#648). Codex already has a systemMessage channel below; this gives the native
// path the same, without making every hook write user-visible.
function writeHookOutput(event, mode, context = '', userMessage = '') {
if (isCopilot) {
// Copilot reads additionalContext on SessionStart; ignores output elsewhere.
process.stdout.write(JSON.stringify(
Expand Down Expand Up @@ -71,6 +78,14 @@ function writeHookOutput(event, mode, context = '') {
{ hookSpecificOutput: { hookEventName: event, additionalContext: context } }));
return;
}
if (userMessage) {
const output = { systemMessage: userMessage };
if (context) {
output.hookSpecificOutput = { hookEventName: event, additionalContext: context };
}
process.stdout.write(JSON.stringify(output));
return;
}
process.stdout.write(context);
}

Expand Down
8 changes: 7 additions & 1 deletion skills/ponytail-help/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ Say "stop ponytail" or "normal mode". Resume anytime with `/ponytail`.

## Configure Default Mode

Default mode = `full`, auto-active every session. Change it:
Default mode = `full`, auto-active every session. `/ponytail off` lasts only for
the current session — the next one starts at the default again. Change it:

**Command**:
```
/ponytail default lite
```

**Environment variable** (highest priority):
```bash
Expand Down
5 changes: 3 additions & 2 deletions skills/ponytail/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ description: >
over-engineering, bloat, boilerplate, or unnecessary dependencies. Do NOT
use for non-coding requests (general knowledge, prose, translation,
summaries, recipes).
argument-hint: "[lite|full|ultra]"
argument-hint: "[lite|full|ultra|off]"
license: MIT
---

Expand All @@ -27,7 +27,8 @@ code is the code never written.

ACTIVE EVERY RESPONSE. No drift back to over-building. Still active if
unsure. Off only: "stop ponytail" / "normal mode". Default: **full**.
Switch: `/ponytail lite|full|ultra`.
Switch: `/ponytail lite|full|ultra|off`. Bare `/ponytail` reports the level.
`/ponytail default <mode>` persists it to new sessions.

## The ladder

Expand Down
45 changes: 45 additions & 0 deletions tests/hooks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -459,4 +459,49 @@ try {
if (prevEnvModeRev === undefined) delete process.env.PONYTAIL_DEFAULT_MODE; else process.env.PONYTAIL_DEFAULT_MODE = prevEnvModeRev;
}

// #648: on native Claude a UserPromptSubmit hook's raw stdout becomes
// additionalContext (model-only), so anything the user asked to see must ride a
// systemMessage. Report, switch and off all qualify; the ruleset injection that
// nobody asked to read must stay raw stdout.
const msgHome = path.join(temp, 'user-message-home');
const msgEnv = {
HOME: msgHome,
USERPROFILE: msgHome,
XDG_CONFIG_HOME: path.join(msgHome, '.config'),
};

function systemMessage(prompt) {
const r = run('ponytail-mode-tracker.js', msgEnv, JSON.stringify({ prompt }));
assert.equal(r.status, 0, r.stderr);
if (!r.stdout.startsWith('{')) return null;
return JSON.parse(r.stdout).systemMessage || null;
}

// Switching says which level took effect.
assert.match(systemMessage('/ponytail ultra') || '', /ultra/, 'a mode switch must be visible to the user');

// Bare /ponytail reports the live level, and does not change it.
assert.match(systemMessage('/ponytail') || '', /ultra/, 'the level report must be visible to the user');
assert.equal(fs.readFileSync(path.join(msgHome, '.claude', '.ponytail-active'), 'utf8'), 'ultra',
'the report must not change the mode');

// Off names the default it will revert to, since clearMode() is session-scoped.
const offMsg = systemMessage('/ponytail off') || '';
assert.match(offMsg, /off/i, 'deactivation must be visible to the user');
assert.match(offMsg, /default is full/, 'off must name the default that revives ponytail next session');

// Same for the natural-language form.
assert.match(systemMessage('stop ponytail') || '', /off/i, '"stop ponytail" must be visible to the user');

// A default change is user-visible too, and distinguishable from a switch.
assert.match(systemMessage('/ponytail default lite') || '', /default set to lite/i,
'a default change must be visible to the user');

// The ruleset injection is not a user message: it stays raw stdout so Claude
// Code keeps treating it as additionalContext.
const activate = run('ponytail-activate.js', msgEnv);
assert.equal(activate.status, 0, activate.stderr);
assert.ok(!activate.stdout.trimStart().startsWith('{'),
'SessionStart ruleset must stay raw stdout, not a systemMessage envelope');

console.log('hook compatibility checks passed');
Loading