Wave 3 only injected the violation error dict back to the LLM as a tool
result. Wave 4 U2 adds a parallel WS event so the frontend PhaseIndicator
can surface violations to the user.
- ReActEngine: add _phase_violations accumulator (list[dict]). Cleared in
reset(). _check_phase_permission appends a structured violation dict
(with new violation_kind field: tool_not_allowed | bash_command_blocked)
before returning the error.
- Add _drain_phase_violations(step) helper that pops pending violations
and returns ReActEvent(event_type="phase_violation", ...) list. Events
carry a shallow copy of the violation dict so callers can't mutate the
accumulator.
- execute_stream: drain after each tool_result yield at all 3 tool
execution sites (parallel, serial-with-confirmation, parsed_calls).
Non-streaming execute() ignores the accumulator (the LLM reinjection
via the error dict is the only signal there).
- chat.py WS handler: new elif branch forwards phase_violation ReActEvents
to the client as {"type": "phase_violation", "data": ...} WS messages.
- Tests: 11 new tests covering accumulator lifecycle, drain semantics,
shallow-copy isolation, and execute_stream event emission for both
tool_block and bash_block paths. 2 new WS forwarding tests pin the
chat.py path (forward + characterization for REACT mode).