# Mechanical Event Module Split Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Split `fabro-workflow`'s oversized event module into focused child modules while preserving `fabro_workflow::event::{...}` as the public API. **Architecture:** Keep `src/event.rs` as a facade that declares child modules and re-exports the existing public symbols. Move code mechanically by responsibility, co-locate tests with the modules they cover, and move `StageScope` to a crate-root module while preserving `fabro_workflow::event::StageScope`. `event/events.rs` intentionally remains the largest file because it keeps the `Event` enum and its exhaustive tracing behavior together. **Tech Stack:** Rust, Tokio, serde/serde_json, chrono, uuid, fabro-types `RunEvent` / `EventBody`, fabro-store `EventPayload`, existing `cargo nextest` workflow tests. --- ## Files - Modify: `lib/crates/fabro-workflow/src/event.rs` - Create: `lib/crates/fabro-workflow/src/event/events.rs` - Create: `lib/crates/fabro-workflow/src/event/names.rs` - Create: `lib/crates/fabro-workflow/src/event/stored_fields.rs` - Create: `lib/crates/fabro-workflow/src/event/convert.rs` - Create: `lib/crates/fabro-workflow/src/event/redaction.rs` - Create: `lib/crates/fabro-workflow/src/event/sink.rs` - Create: `lib/crates/fabro-workflow/src/event/emitter.rs` - Create: `lib/crates/fabro-workflow/src/stage_scope.rs` - Modify: `lib/crates/fabro-workflow/src/lib.rs` - Modify: `docs/internal/events-strategy.md` ## Task 1: Confirm Private Helpers and Build the Facade - [x] Confirm these helpers are not used outside `event.rs` before moving them: ```bash rg -n "StoredEventFields|event_body_from_event|normalized_event_value|redacted_event_value|RunEventCommand|RunEventSinkFuture|RunEventSinkCallback|RunEventTransform|agent_tool_call_id|agent_actor_for_event|default_node_label|node_stored_fields|billed_token_counts_from_llm|stage_status_from_string|epoch_millis" . --glob '!target' --glob '!apps/fabro-web/dist/**' ``` Expected: production hits are limited to `lib/crates/fabro-workflow/src/event.rs`; plan and historical docs may mention the names. - [x] Replace `event.rs` with child module declarations and public re-exports: ```rust mod convert; mod emitter; mod events; mod names; mod redaction; mod sink; mod stored_fields; pub use fabro_types::{EventBody, RunNoticeLevel}; pub use self::convert::{to_run_event, to_run_event_at}; pub use self::emitter::Emitter; pub use self::events::Event; pub use self::names::event_name; pub use self::redaction::{ build_redacted_event_payload, event_payload_from_redacted_json, redacted_event_json, }; pub use self::sink::{ RunEventLogger, RunEventSink, StoreProgressLogger, append_event, append_event_to_sink, }; pub use crate::stage_scope::StageScope; ``` - [x] Add `mod stage_scope;` to `lib.rs`. Do not expose a new `fabro_workflow::stage_scope` public module in this pass; preserve the existing public path through `pub use crate::stage_scope::StageScope` in `event.rs`. ## Task 2: Move Event, Names, Stage Scope, and Stored Fields - [x] Move the `Event` enum, `Event::pull_request_created`, and `Event::trace` into `event/events.rs`. Keep all derives, serde attributes, clippy allowances, variant fields, tracing levels, tracing fields, and `PullRequestRecord` behavior unchanged. - [x] Move `event_name` into `event/names.rs`. Keep the exhaustive match and all returned strings unchanged. - [x] Move `StageScope` into `stage_scope.rs`. Keep constructors and `stage_id()` unchanged, including use of `visit_from_context`. Preserve `fabro_workflow::event::StageScope` by re-exporting it from `event.rs`. - [x] In `stage_scope.rs`, import only the dependencies needed by `StageScope`: `fabro_types::{ParallelBranchId, StageId}`, `crate::context::{Context as WfContext, WorkflowContext}`, and `crate::run_dir::visit_from_context`. Do not import from `crate::event`. - [x] Move stored-field helpers into `event/stored_fields.rs`: - `StoredEventFields` - `default_node_label` - `node_stored_fields` - `stored_event_fields` - `stored_event_fields_for_variant` - `agent_tool_call_id` - `agent_actor_for_event` - [x] Make both `StoredEventFields` and `stored_event_fields` `pub(super)` because `convert.rs` calls the function and reads fields from its return value. Keep `default_node_label`, `node_stored_fields`, `stored_event_fields_for_variant`, `agent_tool_call_id`, and `agent_actor_for_event` private to `stored_fields.rs`. ## Task 3: Move Conversion and Redaction - [x] Move conversion helpers into `event/convert.rs`: - `billed_token_counts_from_llm` - `stage_status_from_string` - `event_body_from_event` - `to_run_event` - `to_run_event_at` - [x] Keep `event_body_from_event` private to `convert.rs`. Import `stored_event_fields` from `event/stored_fields.rs`. Keep `to_run_event` and `to_run_event_at` public through the facade re-export. - [x] Move redaction helpers into `event/redaction.rs`: - `build_redacted_event_payload` - `redacted_event_json` - `normalized_event_value` - `redacted_event_value` - `event_payload_from_redacted_json` - [x] Keep `normalized_event_value` and `redacted_event_value` private. Keep redaction behavior exactly as `RunEvent::to_value() -> normalize_json_value -> redact_json_value`. ## Task 4: Move Sink, Logger, and Emitter Plumbing - [x] Move sink and logger code into `event/sink.rs`: - `append_event` - `append_event_to_sink` - `RunEventSink` - `RunEventSinkFuture` - `RunEventSinkCallback` - `RunEventTransform` - `RunEventCommand` - `RunEventLogger` - `StoreProgressLogger` - [x] Keep `RunEventCommand` and callback type aliases private. Preserve the iterative `RunEventSink::write_run_event` stack logic and redacted JSONL output behavior. - [x] Move emitter code into `event/emitter.rs`: - `epoch_millis` - `EventListener` - `Emitter` - `Debug`, `Default`, and inherent impls - [x] Keep `dispatch_run_event` as `pub(crate)` and keep all public `Emitter` methods unchanged. ## Task 5: Co-locate Tests and Update Docs - [x] Move each existing inline test into the module it characterizes. Keep test names, fixtures, assertions, and async test attributes unchanged. - [x] Use this test placement: - `emitter.rs`: `event_emitter_*` - `sink.rs`: `run_event_sink_*`, `run_event_logger_*`, `append_event_writes_store_event_shape` - `redaction.rs`: `build_redacted_event_payload_*` - `names.rs`: `event_name_matches_new_dot_notation`, `run_archived_event_name_matches_dot_notation` - `stored_fields.rs`: only direct helper tests introduced during the move, if needed; it is acceptable for this module to have no tests - `convert.rs`: all `run_event_*` tests, actor envelope tests, tool call id tests, parallel id tests, stage id tests, metadata snapshot mapping tests, and `stage_scope_populates_stage_id_on_non_stage_events` - `stage_scope.rs`: direct `StageScope` constructor tests introduced during the move, if needed; it is acceptable for this module to have no tests - [x] Update `docs/internal/events-strategy.md` references that say the canonical conversion is in `fabro-workflow/src/event.rs` so they refer to the `fabro-workflow::event` module. Do not change event-strategy rules. - [x] Do not update production call sites outside the event module unless compilation requires an import fix. The intended result is that existing imports such as `crate::event::{Emitter, Event}` and `fabro_workflow::event::{Event, to_run_event}` continue to work. ## Task 6: Verify the Mechanical Split - [x] Run focused event tests: ```bash cargo nextest run -p fabro-workflow event ``` Expected: all matching tests pass. - [x] Run the full workflow crate test suite: ```bash cargo nextest run -p fabro-workflow ``` Expected: all `fabro-workflow` tests pass. - [x] Run format check: ```bash cargo +nightly-2026-04-14 fmt --check --all ``` Expected: no formatting diffs. - [x] Run clippy: ```bash cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings ``` Expected: no warnings. ## Acceptance Criteria - `lib/crates/fabro-workflow/src/event.rs` is a small facade module. - Existing public API paths under `fabro_workflow::event::{...}` still compile. - `StageScope` lives at crate root and remains available from `fabro_workflow::event::StageScope`. - Tests are co-located with the module they cover; there is no catch-all `event/tests.rs`. - Event wire names, envelope metadata, `EventBody` conversion, redaction, JSONL sink output, store payload shape, and emitter dispatch behavior are unchanged. - No macro registry, generated event table, or domain-split `Event` enum is introduced in this pass. - `docs/internal/events-strategy.md` remains accurate after the file split. ## Assumptions - This is a strictly mechanical refactor; deeper cleanup like domain-specific event enums or shared event DTO extraction is out of scope. - `event/events.rs` remains intentionally large because keeping `Event::trace` with `Event` avoids splitting a pure inherent `Event` behavior into a separate file. - Keeping `convert.rs` focused on `Event -> EventBody` body conversion is acceptable even if it remains one of the larger event files. - `RunEventSink`, `RunEventLogger`, and `StoreProgressLogger` remain together in `sink.rs` for this pass; split them later only if that file remains hard to navigate after tests are co-located. - Existing tests are sufficient characterization coverage for this split; new behavior tests are not required unless a moved module exposes an accidental visibility issue.