Repository navigation
refactor(integrations): run the Claude Code and Codex SessionStart flow from the lifecycle package - #2550
Conversation
… Codex as a package Claude Code and Codex now ship the whole integration lifecycle package as powercontext_lifecycle/ in their plugin directories, generated from src/powercontext_integration_lifecycle/. Their hooks and settings import the client configuration, prepared-context, prompt-privacy, and bootstrap-context helpers from it, and their single-file copies are gone. WorkBuddy and Hermes keep their single-file copies where their hooks and installers expect them. A module added to the source now reaches both plugins without a new entry in the generator, which prepares moving their hook flows into the module. The copies use another name than the source, so a process that has already imported the source, such as the test suite, still loads the plugin's own copy. Ruff checks the Codex package copy against Python 3.9, like the single-file copies it replaces. `make integration-lifecycle` deletes the generated modules in a package copy that the source does not have, and names each one; it leaves any other file in a package copy for `make check` to report. `make check` also rejects a subpackage in the source, which the copies would leave out. Elsewhere in the plugins it reports a module named like a single-module copy, as before, a module whose generated-file comment marks it as a copy, such as a single-module copy left behind, and a module identical to a source module, such as one copied straight from the source. A host module that only shares a package module's name is not a copy. Both plugins install their whole directory, so installation is unchanged, and the Claude Code and Codex characterization fixtures pass as recorded. The plugin tests that copy or purge plugin modules now handle the package.
…into the lifecycle package Claude Code and Codex each kept their own diagnostic throttling (hooks/diagnostics.py) and bootstrap receipt state (bootstrap_state.py). The copies differed only in the name of the diagnostics state file and in the two environment variables that locate the receipt directory. Both now live once in src/powercontext_integration_lifecycle/ and reach the plugins through the generated powercontext_lifecycle package. The modules take the host differences as values: DiagnosticThrottle names the state file, and BootstrapState names the two variables. Each plugin declares its values once, in claude_code_host.py and codex_host.py, and its hooks use those instances. State files, environment variables, locking, and failure handling are unchanged, and the Claude Code and Codex characterization fixtures pass as recorded. The plugin tests reach the receipt state through the hooks' declarations. They also set the host's plugin data directory, so a declaration that swaps the two variables fails them. The lock-contention test runs against the source module, which the copies must match.
…nto the lifecycle package Claude Code's hooks/session_start.py and Codex's hooks/session_binding.py ran the same SessionStart flow: resolve the Scope, request the bootstrap package, record its delivery, keep the receipt for the first prompt, and report failures on stderr. Their Scope clients carried the same bounded HTTP helpers, open_bounded, bind_response_deadline, and complete_body, and both plugins defined the same typed Scope binding errors. The response deadline raised socket.timeout in Claude Code, which Python 3.9 needs, and TimeoutError in Codex, the same class on the Python 3.11 and later that Codex runs; the shared helper raises socket.timeout. Both hooks read the bootstrap response, and the error code of a failed request, under the deadline that the request started, so slow headers leave the body only what remains; the flow keeps that deadline. The flow now lives once in src/powercontext_integration_lifecycle/, in session_start.py, with the HTTP helpers in transport.py and the errors in scope_binding_errors.py, which replaces Claude Code's module of that name and Codex's classes in scope_binding.py. A HostProfile in claude_code_host.py and codex_host.py names what differs between the hosts: the integration name, the User-Agent, the diagnostic component, the diagnostic throttle, and the receipt state, which the plugins declared separately before. The hooks keep what is native to their host: they validate the payload, accept their own lifecycles, pass their settings to the flow as plain values, write the package in their output format, and, as before, catch what the flow does not handle. Along with the Scope resolver, they pass the flow their Scope client's reader for the error code of a failed bootstrap request, since the clients stay in the plugins. Codex still binds the session when the lifecycle is missing or unknown. Everything else that used the helpers or the errors, from the prompt hooks to the Claude Code status line, imports them from the package. These are the first package modules that import their siblings. They do so relatively, because the copies use another name than the source, and only modules that WorkBuddy and Hermes do not ship may do it; the source package test enforces both. ty no longer checks the Codex package copy: it resolves powercontext_lifecycle to the identical Claude Code copy, so it cannot resolve the relative imports of the Codex copy. It still checks the source and the Claude Code copy. Requests, receipts, diagnostics, and output are unchanged, and the Claude Code and Codex characterization fixtures pass as recorded. The hooks now read the event identifier before the previous receipt and convert their settings, handoff included, before the Scope is resolved; after a broken pipe they point stdout at the null device before the receipt is cleared. None of these readings can fail, and the null device affects only stdout, so the new order is not observable. The bootstrap tests intercept the Server requests in the shared flow instead of the hooks, and the tests that keep or check a receipt reach it through the hook's HostProfile, with their assertions unchanged. A new test for each host checks that the hook passes its configured credential, time limits, size limit, and handoff to the flow; the characterization fixtures do not cover all of them.
…loads The characterization fixtures give the Claude Code and Codex SessionStart hooks payloads written by hand. The hosts send more: Claude Code adds context and cache fields on resume and fork, and the model and prompt ID after compaction; Codex adds the permission mode. The payloads in tests/fixtures/recorded_host_payloads were recorded from Claude Code 2.1.277 (startup, resume, compact, fork) and Codex 0.165.0-alpha.1.2 (startup, resume, fork), one for each lifecycle; clear was not recorded, nor compact in Codex. The payloads keep every key and value except the session ID, working directory, transcript path, and prompt ID, which follow the characterization fixtures. Each recorded payload replaces the payload of the characterized case in which SessionStart injects the package and the first prompt reports its receipt. It must lead to the same requests, output, and state, apart from the lifecycle that the bootstrap request names. A second test runs each host's hook with a stdout pipe that has no reader, as when the host stops reading. The hook must exit with status 0, report the failed delivery once, and keep no receipt. No other test covers the hooks' redirection of stdout to the null device after a broken pipe: without it, Python reports the broken pipe again at exit and the process exits with status 120. Both tests also pass before the flow moved into the lifecycle package.
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
hireview001
left a comment
There was a problem hiding this comment.
- CI failed: CI result (failure).
- CI failed: Upgrade release gate (failure).
- CI did not complete: tests (3.12, e2e) (cancelled).
- CI failed: tests (3.11, e2e) (failure).
- CI did not complete: tests (3.14, e2e) (cancelled).
- CI did not complete: tests (3.13, unit) (cancelled).
- CI did not complete: tests (3.13, e2e) (cancelled).
- CI did not complete: tests (3.14, unit) (cancelled).
- CI failed: Main (failure). Failed steps: tests (3.13, unit) / Run unit tests; tests (3.11, e2e) / Run remaining end-to-end tests; tests (3.12, e2e) / Run remaining end-to-end tests.
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
hidb4ai
left a comment
There was a problem hiding this comment.
Reviewed d3ec37d. No blocking regression found. The shared lifecycle flow preserves host-specific behavior and existing receipt/diagnostic state without adding database tables or public APIs.
CI still needs to clear: the OpenDAL type-check failure is also present on base 63f021d (base job). This review exercised installed hook subprocesses and Python 3.9 compatibility; it did not rerun interactive host acceptance.
hireview001
left a comment
There was a problem hiding this comment.
- CI failed: CI result (failure).
- CI failed: harness-validation / Acceptance (sqlite) (failure).
- CI failed: harness-validation / Acceptance (oceanbase) (failure).
- CI failed: opendal-package (failure).
- CI failed: Main (failure).
|
CI follow-up for unchanged head The harness runtime and dependency installer are unchanged by this PR; its only |
…ion-lifecycle-session-start-v2
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
hidb4ai
left a comment
There was a problem hiding this comment.
Re-reviewed 229831c. The merge preserves the previously reviewed PR patch, with no new database tables or public APIs and no blocking regression found.
The earlier ACP import failure and OpenDAL type-check diagnostic no longer reproduce in local checks after the master updates. Full CI acceptance is still queued; this approval does not establish that those jobs have passed.
…ion-lifecycle-session-start-v2
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
…ion-lifecycle-session-start-v2
|
The red checks on 993b682 come from one flaky e2e test. The other |
Which issue or RFC does this PR close?
Part of #2250 (plan step 3: move the Claude Code and Codex hook flows onto the lifecycle module). Builds on #2318, which added
src/powercontext_integration_lifecycle/and the copy generator, and on #2317, whose SessionStart classification moves here with the flow. The prompt hooks follow in a separate PR.Rationale for this change
The Claude Code SessionStart hook (
hooks/session_start.py) and the Codex one (hooks/session_binding.py) run the same flow: resolve the Scope, request the bootstrap package, record its delivery, keep the receipt for the first prompt, and report failures on stderr. Each plugin also keeps its own diagnostic throttle, bootstrap receipt state, bounded HTTP helpers, and Scope binding errors. The copies differ only in a few host values, so every fix has to be made twice; #2317 is the latest example.This PR moves that code into the lifecycle package once and gives each plugin a small host declaration for what differs. The hooks keep only what is native to their host.
What changes are included in this PR?
Four commits, each with the details in its message:
Plugins ship the package. Claude Code and Codex now ship the whole package as
powercontext_lifecycle/in their plugin directories, generated fromsrc/powercontext_integration_lifecycle/bymake integration-lifecycle. Their single-file copies of the four helpers are gone; WorkBuddy and Hermes keep theirs. The generator deletes a generated module that the source no longer has, reports other extra files, rejects a source subpackage, and still reports a hand-made copy elsewhere in a plugin.Diagnostics and receipts.
diagnostics.py(DiagnosticThrottle) andbootstrap_state.py(BootstrapState) replace each plugin'shooks/diagnostics.pyandbootstrap_state.py. The host differences (the state file name and the two variables that locate the receipt directory) are declared once per plugin in the newclaude_code_host.pyandcodex_host.py.SessionStart flow.
session_start.py(run_session_start),transport.py(open_bounded,bind_response_deadline,complete_body),scope_binding_errors.py, andhost_profile.py. AHostProfilenames the integration, User-Agent, diagnostic component, throttle, and receipt state. The hooks validate the payload, accept their own lifecycles (Codex still binds the session on a missing or unknown lifecycle), convert their settings to a plainBootstrapConnection, and write the package in their native output. fix(integrations): report Scope resolution failures from prompt hooks #2317's SessionStart behavior is carried in the package as the hooks have it today:HTTPExceptionand short bodies areserver_unavailable; undecodable or oversized bodies areinvalid_response.version_mismatchonly without an error code, and the error code goes into the event.socket.timeout, which Python 3.9 needs for Claude Code.The prompt hooks, the Codex Stop hook, and the Claude Code status line import the helpers and errors from the package.
Shell tests from recorded payloads.
tests/test_session_start_shells.pyruns each hook as a subprocess with payloads recorded from real hosts, intests/fixtures/recorded_host_payloads/: Claude Code 2.1.277 (startup, resume, compact, fork) and Codex 0.165.0-alpha.1.2 (startup, resume, fork). Each recorded payload replaces the payload of the characterized bootstrap case and must lead to the same requests, output, and state. A broken-pipe case per host checks that the hook exits 0, reports the failed delivery once, and keeps no receipt. The tests also pass on the tree before the move.Layout changes in both plugins: new
powercontext_lifecycle/andclaude_code_host.py/codex_host.py; removedpowercontext_client_config.py,hooks/{bootstrap_context,prepared_context,prompt_privacy,diagnostics}.py,bootstrap_state.py, and Claude Code'sscope_binding_errors.py. The #2250 comment that promised no plugin layout changes covered #2318 only.pyproject.toml: ty no longer checks the Codex package copy. It resolvespowercontext_lifecycleto the identical Claude Code copy and cannot resolve the Codex copy's relative imports. The source and the Claude Code copy are still checked.Are there any user-facing changes?
No behavior changes. Requests, receipts, diagnostics, and hook output are unchanged, and the Claude Code and Codex characterization fixtures from #2255 pass as recorded.
scripts/workspace_scope.pyorscripts/scope_binding.pyfails, the last traceback line namespowercontext_lifecycle.scope_binding_errors.ScopeBindingRejectedError(and so on) instead of the plugin-root module. Exit status and stdout are unchanged.For contributors, the SessionStart flow is edited in
src/powercontext_integration_lifecycle/, andmake integration-lifecycleupdates both plugins.How was this change tested?
uv run pytest -n 8 --require-claude-code-python-floor tests/test_hook_characterization.py tests/claude_code_plugin tests/codex_plugin tests/test_session_start_shells.py: 601 passed, 4 skipped (by design intest_client_transport.py). The Python 3.9 tests ran on the macOS system Python 3.9.6. Withtests/test_integration_lifecycle.py,tests/test_hook_diagnostics.py, andtests/test_integration_manifest.pyadded: 675 passed at the tip. The same set passes at each of the four commits, and the Claude Code and Codex characterization fixtures, feat(codex): add a setting that turns prompt-time recall off #2453's Codex cases included, are unchanged at every commit.make integration-lifecycleleaves no diff at any commit;ruff check --no-fixandruff format --checkpass at every commit.-I -S, the Claude Code package copy,claude_code_host,claude_code_settings, and the scripts import.tests/test_session_start_shells.py: 9 passed (7 recorded payloads and a broken-pipe case per host). They also pass on the tree before the move.make check: passes,ty checkincluded.make unit-test: 8405 passed, 419 skipped, 5 failed. Four are intests/test_ci_installation_roles.py, which installs wheels and fails on macOS. The fifth,tests/test_handoff_report.py::test_handoff_report_statement_count_does_not_grow_with_scope_count, passed in three reruns on this branch and on master; this PR changes neither file.git archiveand a local SQLite Server with enforced authentication. Claude Code ran this branch's tip. Codex ran the tip of the follow-up prompt-hook branch, which contains this branch unchanged; its trusted plugin install there is byte-identical to that tip's plugin.claude -p, hooks on/usr/bin/python33.9.6: startup, resume, fork (--fork-session), and compact (/compacton--resume) each injected the bootstrap package, the Server recordedprepare_bootstrap_contextandrecord_bootstrap_delivery, and the model answered the marker that only the package carries. The first prompt took the receipt. With an explicit Scope that does not exist, SessionStart reportedinvalid_responsewithhttp_status404 anderror_codescope_not_foundand injected nothing, and the prompt hook showed thescope_resolvenotice. With the Server stopped, it reportedserver_unavailableand wrote nothing to the diagnostic throttle. Not run: clear, which needs interactive mode.CODEX_HOMEwith the plugin installed from the exported marketplace and its hooks trusted:codex exec --jsonfor startup, thenresumeandforkof that session. Each ran its own SessionStart (startup,resume,fork), and the Server recordedset_scope_binding,prepare_bootstrap_context, andrecord_bootstrap_delivery. The fork's rollout carries the bootstrap package that its own hook injected, and the model answered the bootstrap marker after resume and fork. The first prompt of each session took the receipt, so only lock files were left. With an explicit Scope that does not exist, or with the Server stopped, the hook requested no bootstrap package. Codex does not show SessionStart's stderr diagnostic, so the visible check for these cases is the prompt hook's, in the prompt-hook PR. Not run: clear and compact.tests/fixtures/recorded_host_payloads/.AI usage statement
This PR was prepared with Claude Code (Claude Opus 5.5). It wrote the change and the tests, ran the checks and the real-host runs, and reviewed the change.