Implement common hook input adapter: normalize IDE hooks to canonical format#59
Implement common hook input adapter: normalize IDE hooks to canonical format#59
Conversation
- adapter.js: detect Codex before Claude Code (model+turn_id heuristic) - adapter.test.js: activate Codex tests (remove todo), rename fixture ref to codex-post-tool-use-bash.json (Codex v0.39 hooks Bash only) - claude-code-post-tool-use-write.json: replace hand-crafted fixture with real E2E capture (richer tool_response: type, structuredPatch, originalFile) - codex-post-tool-use-bash.json: schema-derived stub; replace with real capture after `codex login` + re-run capture-hook-fixtures.sh --ide codex - capture-hook-fixtures.sh: new E2E script — spins up temp project, installs dump-stdin.js hooks for Claude Code + Codex, captures/sanitizes/saves fixtures Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- codex-post-tool-use-bash.json: replace schema-derived stub with real stdin capture from Codex v0.118.0. Key findings: - tool_response is a plain string (not object like Claude Code) - permission_mode is "bypassPermissions" (not "default") - transcript_path uses rollout-* naming convention - model + turn_id confirmed present and sanitized - capture-hook-fixtures.sh: switch Codex capture to npx @openai/codex (>=0.118.0 required; brew codex 0.39.0 lacks codex_hooks feature flag) Tests: adapter.test.js 23 pass / 0 fail / 12 todo (Codex GREEN) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a small hooks “runtime” (stdin normalization + a PostToolUse “loose files” nudge) under instructions/r2/core/hooks/, along with fixtures, an E2E fixture capture helper, and plugin-sync filtering so test artifacts aren’t shipped into generated plugins.
Changes:
- Introduces an IDE stdin normalization adapter (
adapter.js) plusnode:testcoverage and captured/stub fixtures. - Adds a
loose-files.jshook that warns when.py/.jsfiles are created/edited outside a detected module boundary. - Updates plugin sync (
scripts/pre_commit.py) to exclude*.test.jsandtest-fixtures/from generated plugin outputs; updates implementation docs and adds planning/handoff docs.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/pre_commit.py | Excludes *.test.js and test-fixtures/ from plugin sync output. |
| plugins/core-cursor/hooks/loose-files.js | Generated hook copy for Cursor plugin. |
| plugins/core-cursor/hooks/adapter.js | Generated adapter copy for Cursor plugin. |
| plugins/core-claude/hooks/loose-files.js | Generated hook copy for Claude plugin. |
| plugins/core-claude/hooks/adapter.js | Generated adapter copy for Claude plugin. |
| plans/plan-manager-skills/hooks-normalization-PLAN.md | Plan for adapter + loose-files hook work. |
| plans/plan-manager-skills/hooks-normalization-HANDOFF.md | Handoff notes, fixtures inventory, and next steps. |
| instructions/r2/core/hooks/test-fixtures/windsurf-post-tool-use-write.json | Stub Windsurf fixture placeholder. |
| instructions/r2/core/hooks/test-fixtures/unknown-ide-input.json | Fixture for unknown-shape error path. |
| instructions/r2/core/hooks/test-fixtures/dump-stdin.js | Helper to dump raw hook stdin to a JSONL file. |
| instructions/r2/core/hooks/test-fixtures/cursor-post-tool-use-write.json | Stub Cursor fixture placeholder. |
| instructions/r2/core/hooks/test-fixtures/copilot-post-tool-use-write.json | Stub Copilot fixture placeholder. |
| instructions/r2/core/hooks/test-fixtures/codex-post-tool-use-write.json | Stub Codex Write fixture placeholder. |
| instructions/r2/core/hooks/test-fixtures/codex-post-tool-use-bash.json | Captured Codex Bash hook fixture. |
| instructions/r2/core/hooks/test-fixtures/claude-code-pre-tool-use-bash.json | Captured Claude Code PreToolUse Bash fixture. |
| instructions/r2/core/hooks/test-fixtures/claude-code-post-tool-use-write.json | Captured Claude Code PostToolUse Write fixture. |
| instructions/r2/core/hooks/test-fixtures/claude-code-post-tool-use-write-subagent.json | Captured Claude Code subagent fixture. |
| instructions/r2/core/hooks/test-fixtures/claude-code-post-tool-use-edit.json | Captured Claude Code PostToolUse Edit fixture. |
| instructions/r2/core/hooks/loose-files.test.js | Unit tests for loose-files hook behavior. |
| instructions/r2/core/hooks/loose-files.js | Loose-file detection hook implementation. |
| instructions/r2/core/hooks/capture-hook-fixtures.sh | E2E script to capture/sanitize fixtures for Claude Code and Codex. |
| instructions/r2/core/hooks/adapter.test.js | Unit tests for adapter detection/normalization/output formatting. |
| instructions/r2/core/hooks/adapter.js | Adapter implementation (Claude Code + Codex detection). |
| agents/IMPLEMENTATION.md | Documents workspace + hooks additions and sync exclusions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Claude Code sends all three of these fields on every hook event | ||
| const CC_SIGNATURE = ['hook_event_name', 'tool_input', 'session_id']; | ||
|
|
||
| /** | ||
| * Detect which IDE sent the input based on field shape heuristics. | ||
| * @param {object} rawInput | ||
| * @returns {'claude-code'} | ||
| * @throws {Error} for null/non-object input or unrecognized IDE shape | ||
| */ | ||
| function detectIDE(rawInput) { | ||
| if (rawInput === null || rawInput === undefined) { | ||
| throw new Error('Invalid input: null or undefined'); | ||
| } | ||
| if (typeof rawInput !== 'object' || Array.isArray(rawInput)) { | ||
| throw new Error('Invalid input: expected a plain object'); | ||
| } | ||
| if (CC_SIGNATURE.every((field) => field in rawInput)) { | ||
| return 'claude-code'; | ||
| } | ||
| throw new Error(`Unsupported IDE: ${JSON.stringify(Object.keys(rawInput))}`); | ||
| } | ||
|
|
||
| /** | ||
| * Normalize any IDE input to Claude Code canonical format. | ||
| * Claude Code input is already canonical — identity pass-through. | ||
| * @param {object} rawInput | ||
| * @returns {object} canonical input | ||
| * @throws {Error} for unsupported IDE shapes |
| if source_file.name.endswith(".test.js"): | ||
| continue | ||
| if "test-fixtures" in relative_path.parts: | ||
| continue |
| run_tests() { | ||
| log_info "Running adapter tests..." | ||
| if node --test "$SCRIPT_DIR/adapter.test.js" 2>&1 \ | ||
| | grep -E "^# (pass|fail|todo)" | head -5; then | ||
| log_ok "adapter.test.js passed" | ||
| else | ||
| log_error "adapter.test.js failed — check output above" | ||
| return 1 | ||
| fi | ||
|
|
||
| log_info "Running loose-files tests..." | ||
| if node --test "$SCRIPT_DIR/loose-files.test.js" 2>&1 \ | ||
| | grep -E "^# (pass|fail|todo)" | head -5; then | ||
| log_ok "loose-files.test.js passed" | ||
| else | ||
| log_error "loose-files.test.js failed" | ||
| return 1 | ||
| fi | ||
| } |
|
|
||
| ### Hooks — IDE Input Normalization | ||
|
|
||
| - Added `instructions/r2/core/hooks/adapter.js`: normalizes IDE stdin to Claude Code canonical format. Exports `detectIDE`, `normalize`, `formatOutput`, `readStdin`. Stub placeholders for Cursor/Codex/Windsurf/Copilot (activated when real fixtures are captured). |
| if (fs.existsSync(path.join(dir, '.git'))) return true; | ||
| if (fs.existsSync(path.join(dir, marker))) return false; |
| if (fs.existsSync(path.join(dir, '.git'))) return true; | ||
| if (fs.existsSync(path.join(dir, marker))) return false; |
| if (fs.existsSync(path.join(dir, '.git'))) return true; | ||
| if (fs.existsSync(path.join(dir, marker))) return false; |
| // Claude Code sends all three of these fields on every hook event | ||
| const CC_SIGNATURE = ['hook_event_name', 'tool_input', 'session_id']; | ||
|
|
||
| /** | ||
| * Detect which IDE sent the input based on field shape heuristics. | ||
| * @param {object} rawInput | ||
| * @returns {'claude-code'} | ||
| * @throws {Error} for null/non-object input or unrecognized IDE shape | ||
| */ | ||
| function detectIDE(rawInput) { | ||
| if (rawInput === null || rawInput === undefined) { | ||
| throw new Error('Invalid input: null or undefined'); | ||
| } | ||
| if (typeof rawInput !== 'object' || Array.isArray(rawInput)) { | ||
| throw new Error('Invalid input: expected a plain object'); | ||
| } | ||
| if (CC_SIGNATURE.every((field) => field in rawInput)) { | ||
| return 'claude-code'; | ||
| } | ||
| throw new Error(`Unsupported IDE: ${JSON.stringify(Object.keys(rawInput))}`); | ||
| } | ||
|
|
||
| /** | ||
| * Normalize any IDE input to Claude Code canonical format. | ||
| * Claude Code input is already canonical — identity pass-through. | ||
| * @param {object} rawInput | ||
| * @returns {object} canonical input | ||
| * @throws {Error} for unsupported IDE shapes |
isolomatov-gd
left a comment
There was a problem hiding this comment.
All tests are to be outside of instructions.
Moreover, you can have all source files outside of instructions too, and only compiled/minified in the instructions (the built final version)
| // | ||
| // Exports (for testability): detectIDE, normalize, formatOutput, readStdin | ||
|
|
||
| const ADAPTERS = [ |
There was a problem hiding this comment.
All-in-one file please
…ove input normalization
This pull request introduces a new IDE input normalization adapter for Rosetta hooks, along with comprehensive test coverage. The main focus is on supporting and normalizing stdin input from various IDEs (starting with Claude Code and Codex) into a canonical format for downstream processing. It also sets up an extensible testing framework and documents these changes in the implementation guide.
IDE Input Normalization and Testing:
adapter.jsmodule toinstructions/r2/core/hooks/for normalizing IDE stdin to the Claude Code canonical format, with detection for Claude Code and Codex, and stubs for Cursor, Windsurf, and Copilot. ExportsdetectIDE,normalize,formatOutput, andreadStdinfunctions.adapter.test.jsusingnode:test, covering all code paths for Claude Code and Codex, and including placeholders for future IDEs. Tests use local JSON fixtures and in-memory streams for input simulation.Workspace Initialization and Documentation:
gain.jsonfor SDLC configuration. Added workspace documentation files:TECHSTACK.md,CODEMAP.md,DEPENDENCIES.md, andASSUMPTIONS.md.Hooks and Automation:
loose-files.jsPostToolUse hook (referenced in documentation), which nudges the AI when loose.pyor.jsfiles lack module markers, with injectedfsfor testability and full test coverage (implementation not shown in this diff).IMPLEMENTATION.mdto reflect new hooks, workspace setup, and test exclusion for plugin sync.