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>
This commit is contained in:
co-authored by
Claude Opus 4.8
Shane McCarron
Emeriko
parent
9d0118df34
commit
b9fa564429
+43
-2
@@ -26,9 +26,14 @@ function run(script, env, input = '') {
|
||||
});
|
||||
}
|
||||
|
||||
// Keep the base env clean so the default-dir checks are deterministic; the
|
||||
// CLAUDE_CONFIG_DIR case sets it explicitly.
|
||||
// Keep the base env clean so the default-dir / native-Claude checks are
|
||||
// deterministic; the CLAUDE_CONFIG_DIR and codex/copilot cases set these
|
||||
// explicitly where needed. run() spreads process.env, so a PLUGIN_DATA /
|
||||
// COPILOT_PLUGIN_DATA leaked from the dev or CI shell would otherwise steer
|
||||
// writeHookOutput into the wrong branch and mis-fire the native assertions.
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
delete process.env.PLUGIN_DATA;
|
||||
delete process.env.COPILOT_PLUGIN_DATA;
|
||||
|
||||
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ponytail-hooks-'));
|
||||
// Runs on normal exit and on assertion-throw exit; force makes it idempotent.
|
||||
@@ -168,4 +173,40 @@ assert.equal(
|
||||
output = JSON.parse(result.stdout);
|
||||
assert.deepEqual(output, {});
|
||||
|
||||
// SubagentStart hook: when ponytail mode is active it injects the ruleset into
|
||||
// each subagent (issue #252). Native Claude must get the hookSpecificOutput JSON
|
||||
// form, not raw stdout, or the context is dropped.
|
||||
const subHome = path.join(temp, 'sub-home');
|
||||
const subFlag = path.join(subHome, '.claude', '.ponytail-active');
|
||||
fs.mkdirSync(path.dirname(subFlag), { recursive: true });
|
||||
const subEnv = { HOME: subHome, USERPROFILE: subHome };
|
||||
|
||||
fs.writeFileSync(subFlag, 'full');
|
||||
result = run('ponytail-subagent.js', subEnv);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
output = JSON.parse(result.stdout);
|
||||
assert.equal(output.hookSpecificOutput.hookEventName, 'SubagentStart');
|
||||
assert.match(
|
||||
output.hookSpecificOutput.additionalContext,
|
||||
/PONYTAIL MODE ACTIVE — level: full/,
|
||||
);
|
||||
|
||||
// No flag → ponytail off → inject nothing (empty stdout, no failure).
|
||||
fs.unlinkSync(subFlag);
|
||||
result = run('ponytail-subagent.js', subEnv);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stdout, '', 'SubagentStart must stay silent when ponytail is off');
|
||||
|
||||
// Codex shares claude-codex-hooks.json, so SubagentStart is reachable under Codex
|
||||
// too — assert the codex branch emits the badge plus hookSpecificOutput.
|
||||
const subCodex = path.join(temp, 'sub-codex');
|
||||
fs.mkdirSync(subCodex, { recursive: true });
|
||||
fs.writeFileSync(path.join(subCodex, '.ponytail-active'), 'full');
|
||||
result = run('ponytail-subagent.js', { HOME: subHome, USERPROFILE: subHome, PLUGIN_DATA: subCodex });
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
output = JSON.parse(result.stdout);
|
||||
assert.equal(output.systemMessage, 'PONYTAIL:FULL');
|
||||
assert.equal(output.hookSpecificOutput.hookEventName, 'SubagentStart');
|
||||
assert.match(output.hookSpecificOutput.additionalContext, /PONYTAIL MODE ACTIVE — level: full/);
|
||||
|
||||
console.log('hook compatibility checks passed');
|
||||
|
||||
Reference in New Issue
Block a user