Fix: parse Grok Build camelCase hook stdin (#646)

Grok was classified as GitHub Copilot, so the design hook skipped every
edit with no-file-path and never ran Stop. Normalize toolInput/sessionId
and treat Stop additionalContext as the Grok product.

Prepared with AI assistance.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Abdul Wahab
2026-08-24 05:28:21 +05:00
co-authored by Cursor
parent c39b6425fa
commit 35ae07339b
7 changed files with 289 additions and 27 deletions
+176
View File
@@ -45,6 +45,7 @@ import {
resolveTargetFiles,
resolveHarness,
normalizeHookEvent,
isStopEvent,
expandScanTargets,
parseStaticStyleImports,
coLocatedStylesheets,
@@ -1493,6 +1494,32 @@ rounded:
assert.equal(out.hookSpecificOutput, undefined);
});
it('handles a Grok Build search_replace event and does not classify it as github (#646)', async () => {
const file = writeFixture('src/Card.tsx', 'noop');
const det = fakeDetector([finding('gradient-text', 1, { name: 'Gradient text' })]);
const grokEvent = {
hookEventName: 'post_tool_use',
sessionId: 'grok-1',
cwd,
workspaceRoot: `${cwd}/`,
toolName: 'search_replace',
toolInput: { file_path: file, old_string: 'a', new_string: 'b' },
toolResult: { type: 'SearchReplace' },
};
const r = await runHook({ stdinJson: JSON.stringify(grokEvent), env: {}, cwd, detector: det });
assert.equal(r.exitCode, 0);
assert.equal(r.audit.harness, 'grok');
assert.notEqual(r.audit.harness, 'github');
assert.equal(r.audit.emitted, true);
assert.equal(r.audit.skipped, undefined);
const out = JSON.parse(r.stdout);
assert.match(out.hookSpecificOutput.additionalContext, /gradient-text/);
const cache = readCache(cwd);
assert.ok(cache.sessions['grok-1'].files[file], 'PostToolUse must mark the file for Stop');
assert.deepEqual(cache.sessions['grok-1'].files[file].findings || [], []);
});
it('handles a GitHub Copilot apply_patch event end-to-end (interactive/cloud path)', async () => {
// The real bug the live test caught: interactive Copilot edits via
// apply_patch (raw patch string in toolArgs), which the matcher and runtime
@@ -2736,6 +2763,47 @@ describe('resolveHarness() / normalizeHookEvent()', () => {
assert.equal(resolveHarness({}, { tool_name: 'Edit', tool_input: { file_path: 'a.tsx' } }), 'claude');
});
it('routes a Grok Build envelope (toolName/toolInput, no toolArgs) to grok, not github (#646)', () => {
const post = {
hookEventName: 'post_tool_use',
sessionId: 's1',
cwd: '/proj',
toolName: 'search_replace',
toolInput: { file_path: '/proj/src/styles.css' },
};
const stop = {
hookEventName: 'stop',
sessionId: 's1',
cwd: '/proj',
reason: 'end_turn',
stopHookActive: false,
};
assert.equal(resolveHarness({}, post), 'grok');
assert.equal(resolveHarness({}, stop), 'grok');
assert.equal(resolveHarness({ IMPECCABLE_HOOK_HARNESS: 'grok' }), 'grok');
assert.equal(isStopEvent(stop), true);
assert.equal(isStopEvent({ hook_event_name: 'Stop' }), true);
assert.equal(isStopEvent(post), false);
});
it('normalizes a Grok search_replace event onto tool_input.file_path + session_id', () => {
const normalized = normalizeHookEvent({
hookEventName: 'post_tool_use',
sessionId: 'g1',
cwd: '/proj',
workspaceRoot: '/proj/',
toolName: 'search_replace',
toolInput: { file_path: '/proj/src/styles.css', old_string: 'a', new_string: 'b' },
toolResult: { type: 'SearchReplace' },
}, '/fallback', 'grok');
assert.equal(normalized.session_id, 'g1');
assert.equal(normalized.cwd, '/proj');
assert.equal(normalized.hook_event_name, 'PostToolUse');
assert.equal(normalized.tool_name, 'search_replace');
assert.equal(normalized.tool_input.file_path, '/proj/src/styles.css');
assert.deepEqual(resolveTargetFiles(normalized, '/proj'), ['/proj/src/styles.css']);
});
it('normalizes a GitHub edit event: JSON-string toolArgs.path -> tool_input.file_path', () => {
const normalized = normalizeHookEvent({
sessionId: 's1',
@@ -3679,6 +3747,7 @@ describe('runHook() — per-edit tiering', () => {
assert.equal(perEditTieringActive({ perEditRules: 'all' }, 'claude'), false);
assert.equal(perEditTieringActive({ perEditRules: 'immediate' }, 'github'), false);
assert.equal(perEditTieringActive({ perEditRules: 'immediate' }, 'cursor'), false);
assert.equal(perEditTieringActive({ perEditRules: 'immediate' }, 'grok'), true);
assert.equal(perEditTieringActive({}, 'claude'), true);
});
@@ -3980,4 +4049,111 @@ describe('runStopHook()', () => {
assert.equal(reentrant.audit.reentrant, true);
assert.equal(reentrant.stdout, '');
});
function grokEditEvent(file, sessionId) {
return {
hookEventName: 'post_tool_use',
sessionId,
cwd,
workspaceRoot: `${cwd}/`,
toolName: 'search_replace',
toolInput: { file_path: file, old_string: 'a', new_string: 'b' },
toolResult: { type: 'SearchReplace' },
};
}
function grokStopEvent(sessionId, reason = 'end_turn') {
return {
hookEventName: 'stop',
sessionId,
cwd,
workspaceRoot: `${cwd}/`,
reason,
stopHookActive: false,
};
}
it('Grok Stop end_turn runs the deep pass over files warmed by camelCase PostToolUse (#646)', async () => {
const sid = 'grok-stop-sid';
const file = write('src/Card.tsx', 'noop');
const det = fakeDetector([
finding('dark-glow', 5),
finding('marketing-buzzword', 3),
]);
const edit = await runHook({ stdinJson: JSON.stringify(grokEditEvent(file, sid)), env: {}, cwd, detector: det });
assert.equal(edit.audit.harness, 'grok');
assert.match(edit.stdout, /dark-glow/);
assert.doesNotMatch(edit.stdout, /marketing-buzzword/);
const stop = await runStopHook({ stdinJson: JSON.stringify(grokStopEvent(sid)), env: {}, cwd, detector: det });
assert.equal(stop.exitCode, 0);
assert.equal(stop.audit.harness, 'grok');
assert.equal(stop.audit.session, sid);
assert.equal(stop.audit.emitted, true);
const out = JSON.parse(stop.stdout);
assert.equal(out.hookSpecificOutput.hookEventName, 'Stop');
// Grok discarded the per-edit stdout, so Stop must still carry the
// immediate-tier finding as well as the deferred remainder.
assert.match(out.hookSpecificOutput.additionalContext, /dark-glow/);
assert.match(out.hookSpecificOutput.additionalContext, /marketing-buzzword/);
});
it('Grok Stop shutdown is observe-only and does not emit a second deep pass (#646)', async () => {
const sid = 'grok-shutdown';
const file = write('src/Card.tsx', 'noop');
const det = fakeDetector([finding('dark-glow', 5)]);
await runHook({ stdinJson: JSON.stringify(grokEditEvent(file, sid)), env: {}, cwd, detector: det });
const stop = await runStopHook({
stdinJson: JSON.stringify(grokStopEvent(sid, 'shutdown')),
env: {}, cwd, detector: det,
});
assert.equal(stop.exitCode, 0);
assert.equal(stop.stdout, '');
assert.equal(stop.audit.skipped, 'stop-reason');
assert.equal(stop.audit.reason, 'shutdown');
});
it('Grok stopHookActive:true exits silent after camelCase normalize (#646)', async () => {
const sid = 'grok-active';
const file = write('src/Card.tsx', 'noop');
const det = fakeDetector([finding('marketing-buzzword', 3)]);
await runHook({ stdinJson: JSON.stringify(grokEditEvent(file, sid)), env: {}, cwd, detector: det });
const active = { ...grokStopEvent(sid), stopHookActive: true };
const stop = await runStopHook({ stdinJson: JSON.stringify(active), env: {}, cwd, detector: det });
assert.equal(stop.exitCode, 0);
assert.equal(stop.stdout, '');
assert.equal(stop.audit.skipped, 'stop-hook-active');
});
it('hook.mjs routes Grok camelCase stop stdin into runStopHook (#646)', async () => {
const sid = 'grok-script-stop';
const file = write('src/hero.css', [
'.hero {',
' background: linear-gradient(#f00, #00f);',
' -webkit-background-clip: text;',
' color: transparent;',
'}',
'',
].join('\n'));
const edit = await runHook({ stdinJson: JSON.stringify(grokEditEvent(file, sid)), env: {}, cwd });
assert.equal(edit.audit.harness, 'grok');
assert.equal(edit.audit.emitted, true);
const env = { ...process.env };
delete env.IMPECCABLE_HOOK_DEPTH;
delete env.CLAUDE_HOOK_DEPTH;
const out = execFileSync(process.execPath, [path.resolve('skill/scripts/hook.mjs')], {
cwd,
input: JSON.stringify(grokStopEvent(sid)),
env,
encoding: 'utf-8',
});
const payload = JSON.parse(out);
assert.equal(payload.hookSpecificOutput.hookEventName, 'Stop');
assert.match(payload.hookSpecificOutput.additionalContext, /gradient-text/);
});
});