You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Managed hooks (docs-ai 064, S3a/S3b) are a contract with external agent binaries, and that contract moves. While closing S3b, three drifts surfaced that no unit test could have caught, because the stubs encode exactly the assumptions that rotted:
Layer
What drifted
Where it was found
Process shape
Droid ≥ 0.202 forks a droid exec engine child; hooks descend from it
009-s3b-action.md
Preflight protocol
Codex 0.149.1 app-server exits on stdin EOF and drops queued config/read
007-s3a-action.md follow-up
Injection surface
Qoder flag hooks are folder-trust gated (research doc had said otherwise)
009-s3b-action.md
The unit suite holds Prowl-side invariants well (argv/settings rendering, payload decoding, the observation store state machine, caller attribution). It structurally cannot detect binary drift. The only test touching a real binary today is CodexConfigReadLiveContractTests, env-gated and Codex-only — and it silently reports success when the variable is passed without the TEST_RUNNER_ prefix, so it may never have run in earlier gates.
When
After S3c lands. S3c adds Pi, Oh My Pi, and OpenCode to the tier-A set, so the drift guard should be designed once for all eight runtimes rather than retrofitted per slice.
Scope
T0 — Version attestation (cheap, zero runtime cost)
An attestation record (e.g. agent-attestation.json next to the research matrix) listing, per runtime, the version the contract last passed against and the date.
make agent-versions: compares installed versions (--version of each binary) with the attestation; newer-than-attested → warning, with a hint to run T1.
Optional: a scheduled check of npm/brew latest versions so a new release is noticed before someone upgrades.
One test per runtime, using its one-shot mode (claude -p, codex exec, droid exec, qodercli -p, Copilot's headless mode, Pi/OMP/OpenCode equivalents):
Build argv and settings through the production renderers (ManagedHookRendering, AgentManagedHookPreparer) — the test must exercise the same injection the app performs.
Point the hook command at the real bundled CLI against a stub Unix socket (the fakesock approach used during S3b), or at a capture script.
Assert the received frame decodes through the productionAgentNativeHookPayload decoder with the expected runtime, native event → signal mapping, cwd, and session id.
Assert SessionStart / SessionEnd without a model turn; assert Stop / agent-turn-complete with one turn on the cheapest model when credentials are present.
Include the Codex preflight (config/read) as it exists today.
make test-agent-contracts bakes the TEST_RUNNER_ prefix in so the gate can never be silently skipped; passing runs update the T0 attestation.
Run locally on demand: before a release, and whenever a runtime is upgraded. Not a CI job (credentials, spend).
Non-goals
Interactive E2E through the app (isolated Debug instance + prowl create tab --profile + trust prompts). It is the only layer that covers process shape, but it drifts on its own (TUI prompts, trust dialogs, flaky deferred surface creation) and costs GUI time per run. Keep the S3b sweep script as a manual release-runbook aid; do not productize it now.
Field telemetry ("hook registered but never verified") — deferred.
Acceptance
make agent-versions reports the attestation state for every tier-A runtime.
make test-agent-contracts runs one real-binary contract per tier-A runtime and fails loudly when a binary is missing, unauthenticated, or drifts.
The research matrix's baseline versions are derived from the attestation record instead of being hand-edited.
Why
Managed hooks (docs-ai 064, S3a/S3b) are a contract with external agent binaries, and that contract moves. While closing S3b, three drifts surfaced that no unit test could have caught, because the stubs encode exactly the assumptions that rotted:
droid execengine child; hooks descend from it009-s3b-action.mdapp-serverexits on stdin EOF and drops queuedconfig/read007-s3a-action.mdfollow-up009-s3b-action.mdThe unit suite holds Prowl-side invariants well (argv/settings rendering, payload decoding, the observation store state machine, caller attribution). It structurally cannot detect binary drift. The only test touching a real binary today is
CodexConfigReadLiveContractTests, env-gated and Codex-only — and it silently reports success when the variable is passed without theTEST_RUNNER_prefix, so it may never have run in earlier gates.When
After S3c lands. S3c adds Pi, Oh My Pi, and OpenCode to the tier-A set, so the drift guard should be designed once for all eight runtimes rather than retrofitted per slice.
Scope
T0 — Version attestation (cheap, zero runtime cost)
agent-attestation.jsonnext to the research matrix) listing, per runtime, the version the contract last passed against and the date.make agent-versions: compares installed versions (--versionof each binary) with the attestation; newer-than-attested → warning, with a hint to run T1.T1 — Headless contract tests (real binaries, no TUI, deterministic assertions)
One test per runtime, using its one-shot mode (
claude -p,codex exec,droid exec,qodercli -p, Copilot's headless mode, Pi/OMP/OpenCode equivalents):ManagedHookRendering,AgentManagedHookPreparer) — the test must exercise the same injection the app performs.fakesockapproach used during S3b), or at a capture script.AgentNativeHookPayloaddecoder with the expected runtime, native event → signal mapping, cwd, and session id.SessionStart/SessionEndwithout a model turn; assertStop/agent-turn-completewith one turn on the cheapest model when credentials are present.config/read) as it exists today.make test-agent-contractsbakes theTEST_RUNNER_prefix in so the gate can never be silently skipped; passing runs update the T0 attestation.Non-goals
prowl create tab --profile+ trust prompts). It is the only layer that covers process shape, but it drifts on its own (TUI prompts, trust dialogs, flaky deferred surface creation) and costs GUI time per run. Keep the S3b sweep script as a manual release-runbook aid; do not productize it now.Acceptance
make agent-versionsreports the attestation state for every tier-A runtime.make test-agent-contractsruns one real-binary contract per tier-A runtime and fails loudly when a binary is missing, unauthenticated, or drifts.Refs
docs-ai/064-agent-completion-signals/007-s3a-action.md,009-s3b-action.md,research-agent-completion-signals.mdsupacodeTests/CodexConfigReadLiveContractTests.swift— the pattern to generalize