Files
ponytail/hooks/ponytail-runtime.js
T
b9fa564429 feat: inject ponytail ruleset into subagents via SubagentStart hook (#254)
* feat: inject ponytail ruleset into subagents via SubagentStart hook

SessionStart additionalContext is parent-thread only, so every Task-spawned
agent ran ponytail-unaware. Add a SubagentStart hook that injects the active
ruleset into each subagent, reusing getPonytailInstructions. Native Claude
needs the hookSpecificOutput JSON form (not raw stdout), so writeHookOutput
grows a SubagentStart branch; readMode exposes the live flag.

Workflow- and team-spawned coverage is undocumented upstream; verify in a
fresh session once installed.

Closes #252

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1dMag33yz1Kf1jS24Aira

* fix: address QA round 1 — make hook tests hermetic + cover Codex SubagentStart

QA round 1 (panel + deepseek-v4-pro) found two real test issues:
- The new subagent block (and the pre-existing claudeEnv block) used a no-op
  `delete env.PLUGIN_DATA`; run() spreads process.env, so a PLUGIN_DATA /
  COPILOT_PLUGIN_DATA leaked from the shell would steer writeHookOutput into the
  codex/copilot branch and silently mis-fire the native-Claude assertions.
  Fixed at the source: neutralize both vars once at the top, like CLAUDE_CONFIG_DIR.
- The Codex SubagentStart branch (claude-codex-hooks.json is shared by both plugin
  manifests) had zero coverage. Added a codex-path assertion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1dMag33yz1Kf1jS24Aira

* fix: use PowerShell-safe command form for SubagentStart hook

Match the post-#265 'node ...; exit 0' form used by the sibling hooks. The old 'command -v node ... || exit 0' form fails tests/hooks-windows.test.js (POSIX-guard and non-blocking asserts) once this branch merges onto current main.

---------

Co-authored-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Emeriko <dietrich.gebert@gmail.com>
2026-06-24 04:19:14 +02:00

69 lines
1.9 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const { getClaudeDir } = require('./ponytail-config');
const STATE_FILE = '.ponytail-active';
const isCopilot = Boolean(process.env.COPILOT_PLUGIN_DATA);
const isCodex = !isCopilot && Boolean(process.env.PLUGIN_DATA);
let stateDir = getClaudeDir();
if (isCodex) stateDir = process.env.PLUGIN_DATA;
if (isCopilot) stateDir = process.env.COPILOT_PLUGIN_DATA;
const statePath = path.join(stateDir, STATE_FILE);
function setMode(mode) {
fs.mkdirSync(path.dirname(statePath), { recursive: true });
fs.writeFileSync(statePath, mode);
}
function clearMode() {
try { fs.unlinkSync(statePath); } catch (e) {}
}
// Live mode written by activate/mode-tracker. Absent flag = ponytail off.
function readMode() {
try {
return fs.readFileSync(statePath, 'utf8').trim() || null;
} catch (e) {
return null;
}
}
function writeHookOutput(event, mode, context = '') {
if (isCopilot) {
// Copilot reads additionalContext on SessionStart; ignores output elsewhere.
process.stdout.write(JSON.stringify(
event === 'SessionStart' && context ? { additionalContext: context } : {}));
return;
}
if (isCodex) {
const output = { systemMessage: `PONYTAIL:${mode.toUpperCase()}` };
if (context) {
output.hookSpecificOutput = {
hookEventName: event,
additionalContext: context,
};
}
process.stdout.write(JSON.stringify(output));
return;
}
// Native Claude: SessionStart accepts raw stdout, but SubagentStart needs the
// hookSpecificOutput JSON form or the context is dropped.
if (event === 'SubagentStart') {
process.stdout.write(JSON.stringify(
{ hookSpecificOutput: { hookEventName: event, additionalContext: context } }));
return;
}
process.stdout.write(context);
}
module.exports = {
clearMode,
isCodex,
isCopilot,
readMode,
setMode,
writeHookOutput,
};