Repository navigation
Conversation
Pooling the budget across agents made the asymmetry visible: a Codex turn was scored 0-10, while a Claude Code or Gemini CLI turn was counted at a flat two points, because only Codex had a hook installed. One hook script and one scorer already served every agent; only the installer was Codex-only. A single Agent registry now describes each product's config file, events, and schema quirks, and build_hooks() writes each one's own shape: codex $CODEX_HOME/hooks.json SessionStart, UserPromptSubmit claude $CLAUDE_CONFIG_DIR/settings.json SessionStart, UserPromptSubmit gemini $GEMINI_DIR/settings.json SessionStart, BeforeAgent Codex's mapping is byte-for-byte what hooks.json.template and install_windows.ps1 write, including the commandWindows pair. The new agents need their own handling: - Claude Code has no async flag, so its entries are plain; the hook's own 25s scorer timeout keeps it inside UserPromptSubmit's 30s default. - Gemini CLI counts timeout in milliseconds, names hooks instead of matching them, and has no UserPromptSubmit or statusMessage field. - Codex's payload carries turn_id, which the hook already maps to Codex, so only the other two get HEADROOM_AGENT in the command line. Gemini's id is "antigravity", the name the adapter registry and the ledger already use for the ~/.gemini home the two Google tools share. - Antigravity's own hook system has no prompt event (PreInvocation fires before every model call, which would charge one prompt several times), so only Gemini CLI is wired up. --agents takes auto (default: Codex plus every other agent already installed here), all, or a comma list. Every config is parsed before any is written, so an unparseable file in one agent cannot leave another half-installed, and --uninstall never deletes a settings file it did not create. install.sh is the POSIX wrapper, and test_installer.py covers the Codex contract plus the two new agents, including an end-to-end check that one turn from either agent lands in the same ledger with the same score.
cat0825
force-pushed
the
multi-agent-hooks
branch
from
September 26, 2026 03:03
590c18f to
3f13750
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #8, which pooled one budget across every local agent.
The asymmetry this fixes
#8 made the asymmetry visible: a Codex turn was scored 0-10 by the local scorer, while a Claude Code or Gemini CLI turn was counted at a flat two points — because only Codex had a hook installed. One hook script and one scorer already served every agent; only the installer was Codex-only.
So "the same scoring standard for every agent" is one installer away. This PR is that installer.
What it does
A single
Agentregistry describes each product's config file, event names, and schema quirks, andbuild_hooks()writes each one's own shape:$CODEX_HOME/hooks.jsonSessionStart,UserPromptSubmit$CLAUDE_CONFIG_DIR/settings.jsonSessionStart,UserPromptSubmit$GEMINI_DIR/settings.jsonSessionStart,BeforeAgentCodex's mapping is byte-for-byte what
hooks.json.templateandinstall_windows.ps1write, including thecommandWindowspair, so #2's and #7's installer contract is unchanged. The new agents need their own handling:asyncflag, so its entries are plain. The hook's own 25s scorer timeout keeps it insideUserPromptSubmit's 30s default, and itsUserPromptSubmittakes no matcher.UserPromptSubmitorstatusMessagefield. Getting the unit wrong is silent — the hook is simply killed and stops charging.turn_id, which the hook already maps to Codex, so only the other two getHEADROOM_AGENTin the command line. Gemini's id isantigravity, the name the adapter registry and the ledger already use for the~/.geminihome the two Google tools share.PreInvocationfires before every model call, which would charge one prompt several times. Antigravity still counts toward the cap through theantigravityadapter.Also fixes
main's red CI (second commit)mainhas been failing on Python 3.10 and 3.11 since #8 merged — see run 36122097975, which is the merge commit itself. The cause is not the installer:--agentbelongs to the subcommands while--agent-homeand--agentsbelong to the top-level parser. Up to Python 3.11, argparse resolves option abbreviations while pre-scanning every argument, so it read the subcommand's--agentas an ambiguous abbreviation of the two top-level options and exited 2 — before the subcommand ever saw it. The hook shells out with exactly that argv, so on 3.10/3.11 no turn ever charged: the hook loggedscorer_process_failedand failed open, which is silent by design. 3.12 and 3.13 changed the pre-scan, which is why only the two older versions failed.allow_abbrev=Falseon the top-level parser is the documented fix for a subcommand option that is a prefix of a top-level one. Verified against CPython 3.10.20 and 3.11.14: both exit 2 before the change, both charge after it. The one visible trade-off is that long-option abbreviations (--statefor--state-path) stop working; every caller in the repo, the hook included, already passes full flags. A regression test asserts the hook's exact argv parses and that--agents/--agent-homestill do.Safety
--agentstakesauto(default: Codex plus every other agent that already has a home directory here, so it never creates~/.claudeor~/.geminifor a tool you do not use),all, or a comma list.Every config is parsed before any is written, so an unparseable file in one agent cannot leave another half-installed. Every written file is backed up first, unrelated hook entries are preserved, re-running is idempotent, and
--uninstallnever deletes a settings file it did not create.Tests
test_installer.pyis new and self-contained: the Codex contract, the two new agents' schemas,--agentsselection, merge/uninstall round-trips, and an end-to-end check that drives the real hook script with a documented Claude Code payload and a documented Gemini CLI payload, asserting both land in the same ledger with the same score and that a redelivered hook charges once.Full suite passes on CPython 3.10, 3.11, 3.12 and 3.13, and all 12 CI jobs are green.
Overlap with #7
#7 also adds
skills/headroom/hooks/install_hooks.pyandinstall.sh, as a Codex-only installer. This PR is a superset of that file and ofinstall.sh. If #7 lands first I will rebase and keep #7'stest_platform.pysplit; if this lands first, #7 can drop those two files. Nothing in #7'stest_platform.pyassertions is broken by this change — the Codex output is unchanged.