feat(adapters): codex T4 — hook wiring for automatic session capture#396
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adds full Codex adapter integration up through T4 by wiring project-local hooks to automatically ingest Codex session rollouts into vouch’s existing review-gated capture flow, including idempotent “refresh pending proposal” behavior for per-turn Stop events.
Changes:
- Introduces Codex rollout parsing + ingestion (
vouch capture ingest-codex, plus--hookmode) and pending-proposal refresh semantics. - Extends the Codex adapter tiers (T1–T4): TOML deep-merge for
.codex/config.toml, fenced AGENTS.md snippet, project-local skills, and.codex/hooks.jsonStop hook wiring viajson_merge. - Adds storage support for in-place rewrites of existing pending proposals (
update_proposal) and expands installer logic (fenced refresh + TOML merge utilities).
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_install_adapter.py | Adds coverage for Codex T2/T3/T4 install behaviors, fenced refresh, json_merge hooks, and toml_merge config behavior. |
| tests/test_codex_rollout.py | Adds end-to-end tests for rollout parsing, ingest behavior, hook payload handling, and --latest resolution. |
| tests/fixtures/codex/rollout-no-meta.jsonl | Adds fixture for missing-session-meta error path. |
| tests/fixtures/codex/rollout-basic.jsonl | Adds representative rollout fixture used for parser/ingest coverage. |
| src/vouch/storage.py | Adds KBStore.update_proposal() for in-place pending proposal rewrites. |
| src/vouch/install_adapter.py | Adds toml_merge, fenced refresh behavior, TOML merge + minimal TOML serializer, and install path wiring. |
| src/vouch/codex_rollout.py | Implements rollout parsing, ingest/dedup/update logic, and Stop-hook payload handling. |
| src/vouch/cli.py | Adds capture ingest-codex CLI command with --latest and --hook modes, and wires CodexRolloutError into CLI error handling. |
| adapters/codex/README.md | Documents Codex T3 skills and T4 automatic capture via hooks.json, including rationale vs notify. |
| adapters/codex/install.yaml | Defines Codex tiered install manifest (T1 toml_merge, T2 fenced snippet, T3 skills, T4 hooks.json json_merge). |
| adapters/codex/hooks.json | Provides the default Stop hook command wiring to vouch capture ingest-codex --hook. |
| adapters/codex/AGENTS.md.snippet | Adds Codex AGENTS.md snippet establishing vouch usage + review-gate invariants. |
| sessions = (codex_home or default_codex_home()) / "sessions" | ||
| if not sessions.is_dir() or not session_id.strip(): | ||
| return None | ||
| matches = sorted(sessions.rglob(f"rollout-*-{session_id}.jsonl"), reverse=True) | ||
| return matches[0] if matches else None |
| verbs = {verb for verb, _ in matches} | ||
| verb = "Created" if verbs == {"Add"} else "Edited" |
| (codex_dir / "hooks.json").write_text(json.dumps({ | ||
| "hooks": { | ||
| "Stop": [ | ||
| {"hooks": [{"type": "command", "command": "my-own-stop-hook"}]} | ||
| ] | ||
| } | ||
| })) |
| day.mkdir(parents=True) | ||
| target = day / "rollout-2026-07-01T09-00-00-sess-42.jsonl" | ||
| target.write_text("{}", encoding="utf-8") | ||
| (day / "rollout-2026-07-01T10-00-00-sess-43.jsonl").write_text("{}") |
0f3de2a to
205c5e4
Compare
|
addressed the copilot review: this branch was rebuilt on the updated #394 and the fixed #395 ingest, so it also carries those reviews' fixes. |
with `vouch capture ingest-codex` available, codex can self-capture the way claude-code T4 does. the ticket assumed the `notify` setting in config.toml, but codex only honours notify in user-global config — it is a restricted key in project-local layers — and the vouchdev#179 rule forbids touching ~/.codex. codex's hooks system is the project-local equivalent: `.codex/hooks.json` (loaded in trusted projects) fires Stop when a turn completes, with a payload carrying the session id and transcript path. T4 ships that file through json_merge, so an existing user hooks.json is deep-merged into, never overwritten, and re-runs don't duplicate the hook. the handler is a new --hook mode on ingest-codex: read the stop payload from stdin, resolve the session's rollout (transcript_path when present, else by session id in the rollout filename), ingest idempotently, and exit 0 no matter what — the same never-break-the- host rule capture observe follows. stop fires per turn, not per session end, so the finalize policy the ticket left open is resolved as idempotent re-ingest: the dedup guard now refreshes a session's still-PENDING proposal in place when the rollout grew (same id, updated summary, audit-logged), reports an unchanged rollout as a no-op, and never resurrects a proposal a human already decided. storage gains update_proposal, a pure-I/O in-place rewrite of a pending proposal file. wiring only — everything still lands through propose_page; nothing touches approve(). closes vouchdev#388
205c5e4 to
79ed4c1
Compare
follows #395: with
vouch capture ingest-codexavailable, codex can self-capture the way claude-code T4 does. one design correction against the ticket, discovered while checking the current codex cli: thenotifysetting the ticket proposed is a restricted key — codex honours it only in user-global~/.codex/config.tomland ignores it in project-local layers, and the #179 rule forbids a project-scoped install from touching home-directory state. codex's hooks system is the project-local replacement (notify itself is now a legacy shim over it):<project>/.codex/hooks.jsonis loaded in trusted projects and firesStopwhen a turn completes, with a json payload on stdin carryingsession_id,cwd, andtranscript_path.so T4 ships
.codex/hooks.jsonregistering aStophook that runsvouch capture ingest-codex --hook. the file installs through the existingjson_mergemachinery — codex's hooks.json shape is the same event/group/command structure as claude-code's settings hooks — which gives the ticket's "never silently overwrite an existing user hook" for free: a pre-existing hooks.json is deep-merged into, the user's own hooks stay, and re-runs don't duplicate ours. as a side effect this drops the planned dependency on the toml_merge pr (#392) for the wiring itself, though this branch still stacks on the codex adapter chain for the shared manifest.the new
--hookmode reads the stop payload from stdin, resolves the session's rollout (transcript_pathwhen it points at a jsonl, else by the session id embedded in the rollout filename), ingests idempotently, and exits 0 no matter what — the same never-break-the-host rulecapture observefollows; garbage stdin, a missing kb, or an unresolvable session are all silent no-ops.Stopfires per turn, not per session end, so the finalize policy the ticket left open is resolved as idempotent re-ingest: the #387 dedup guard now updates rather than duplicates — a session that grew since the last ingest refreshes its still-PENDING proposal in place (same proposal id, updated summary,proposal.page.updateaudit event), an unchanged rollout reportsalready-ingested, and a proposal the human already approved or rejected is never resurrected or mutated. storage gainsupdate_proposal, a pure-I/O in-place rewrite of a pending proposal file; the only-pending rule lives with the caller, keeping storage logic-free.wiring only — everything still lands through
propose_page, nothing touchesapprove(). tests cover the install paths (fresh write, merge next to a user hook, idempotent re-run without duplication) and the hook-driven ingest (payload handling, session-id resolution, grown-session refresh, decided-proposal protection, audit event, exit-0 guarantees with garbage stdin and no kb). verified end-to-end against a real codex 0.142.0 rollout through a stdin stop payload: proposal filed, re-run no-op, garbage exits 0.stacks on #392/#393/#394 (shared codex manifest) plus #395 (the ingest command) — merge after them; the diff reduces to the T4 change once they land.
closes #388