Repository navigation
Description
Symptom in cargo-hauler
cargo-hauler's four event routes (src/events/**) cannot use the payload the framework hands them without a host-specific bridge. ScriptedAlchemy/cargo-hauler#43 (framework-mode audit, finding 7) records it as a framework gap, not plugin drift: AgentEventRouteProps.native is the raw host envelope and no canonical payload accessor is exported, so the plugin keeps src/lib/event-support.ts to paper over:
- session id:
session_id(Claude/Codex) vsconversation_id(Cursor) —event-support.ts:56,:99 - tool result:
tool_responseobject (Claude/Codex) vstool_outputJSON string (Cursor) —event-support.ts:69-77 - stop re-entry:
stop_hook_activeboolean vs Cursorloop_countnumber —event-support.ts:87-95 tool_name,tool_input,tool_use_id,cwdread by key fromnative—event-support.ts:55-59
Every cross-host plugin with a tool/before, tool/after, or stop route has to rediscover and re-implement the same table.
Root cause in agent-bundle (main 10a98a0fb)
packages/agent-bundle/src/routes/public.ts:53—AgentEventNativePayload = Readonly<Record<string, unknown>>;:64-68—AgentEventRoutePropsis{ canonical, native, signal }wherecanonical(:44-50) carries only identity (event,idempotencyKey,observedAt,provenance,sequence), never payload.- The framework already knows every per-host field: the wrapper validates the envelope per host and per event (
website/docs/en/guide/authoring/hooks.mdx:229"validates it per host and per event"), the pinned capability tables enumerate the input fields (capabilities/codex-0.147.0.json:812-815,capabilities/cursor-2026-08-28.json:593), andhook-contract.ts:148-149buildstool_input/tool_namewhen encoding playground input. Only the read side is left to the route. await agent()gives host/session/actor/workspace asObservedaxes, so session identity is partly covered; tool name/input/response and stop re-entry are not.
Workaround currently in cargo-hauler
src/lib/event-support.ts (whole module): beforeShellEventFrom, afterShellEventFrom, stopHoldEventFrom, hookContextFrom, consumed by src/events/tool/before.tsx, src/events/tool/after.tsx, src/events/stop.tsx, src/events/session/start.tsx.
Proposed fix
Add a typed, per-family canonical payload next to native (keep native for anything not modelled):
interface AgentEventRouteProps<F extends CanonicalAgentEvent = CanonicalAgentEvent> {
readonly canonical: AgentEventCanonicalIdentity;
readonly payload: CanonicalEventPayload[F]; // new
readonly native: AgentEventNativePayload;
readonly signal: AbortSignal;
}
// tool/before: { toolName, toolInput: JsonValue, toolUseId?, cwd? }
// tool/after: tool/before + { toolResponse: JsonValue } (Cursor's string parsed when it is JSON, else kept as string)
// stop / agent/stop: { reentry: boolean } (stop_hook_active || loop_count > 0)
// session/start: { source? }- Derived in the generated wrapper from the already-validated envelope, using the per-host tables the adapters own; fields a host does not report are typed
Observed/optional, never fabricated (same rule ascanonical). - Surfaced in the Workbench playground and
agent-bundle/testrunEventRouteso fixtures exercisepayload, not raw keys. - Docs:
website/docs/{en,zh}/guide/authoring/hooks.mdxroute props section; generated events reference unchanged.
Acceptance
- A
tool/beforeroute reading onlypayload.toolName/payload.toolInputbehaves identically under the checked-in Claude, Codex, and Cursor native fixtures. - A
stoproute readingpayload.reentryseestruefor Claude/Codexstop_hook_active: trueand Cursorloop_count: 1. - cargo-hauler can delete
src/lib/event-support.ts(exceptdecisionValue) once pinned to the fix.
Activity
feat(events): give event routes a canonical per-family payload beside…
feat(events): give event routes a canonical per-family payload beside…
feat(events): give event routes a canonical per-family payload beside…
feat(events): give event routes a canonical per-family payload beside…
feat(events): canonical per-family payload on event routes beside the…