axagent experiencelivev0.31.0

architecture decision record

Internal working engineering record - kept for reference, not a user-facing guide.

Hooks ownership split: hooks-sdk owns definition + fire path, axctl owns config + analysis

Status: Accepted (2026-06-13)

Hook functionality had grown in two places at once - packages/hooks-sdk (the fire-path runtime consumed from ~/.ax/hooks via a file: dep) and apps/axctl/src/hooks (the CLI surface) - with no stated boundary. The locked split:

The dependency arrow only ever points axctl → hooks-sdk. The sdk stays dependency-light (effect only, pinned not catalog:) because ~/.ax/hooks workspaces resolve it outside the monorepo.

Consequence: routing-table dedup

route-dispatch.ts and queries/routing-table-io.ts each carried their own schema + parse of routing-table.json, and the default class seed was duplicated verbatim (DEFAULT_TABLE in the hook, ROUTING_CLASSES in dispatch-analytics.ts) with mirror-me comments. Both now import one module, @ax/hooks-sdk/routing-table; ROUTING_CLASSES is an alias of the sdk's DEFAULT_ROUTING_TABLE, so the hook's fallback and the compile seed cannot drift. The defaults live in the sdk rather than axctl because the fire path must work before any ax routing compile step exists; axctl re-exports them, so no import cycle (this also broke the prior routing-table-io ↔ dispatch-analytics cycle).

The two read semantics are deliberately both kept, side by side in the one module: the fire path does a whole-table fail-open decode (any problem → built-in defaults; a corrupt table must never wedge the agent), while the compile side normalizes row-by-row and returns null on a structurally bad file (so ax routing compile can refuse to overwrite it).

Also folded in: route-dispatch no longer requires GitEnv - routing is pure table matching on tool input. HookDefinition.run's R-channel stays typed as GitEnv (covariant, never is assignable), so hooks that need git state keep it and pure hooks just don't yield it.

Trade-offs