Extract the WS path's inline phase_policy construction into a shared
_build_phase_engine helper so the REST send_message endpoint can reuse
it. Replace the former 501 stub with actual PLAN_EXEC execution:
- REST POST /chat/sessions/{id}/messages with execution_mode=plan_exec
now builds a phase-policy-backed ReActEngine, calls execute()
(non-streaming), and returns a MessageResponse.
- KTD5: PLAN_EXEC bypasses execute_with_fallback_chain — phase policy
and fallback chain are mutually exclusive.
- When plan_exec.enabled=False, REST falls through to the REACT path
(matching WS behavior).
- WS path refactored to call the same helper; behavior unchanged.
Tests:
- Replace TestRestPlanExec501 with TestRestPlanExec (happy path, bad
config → 500, disabled → falls through to REACT, REACT mode unchanged).
- Add TestBuildPhaseEngineHelper covering all return branches:
not-PLAN_EXEC, disabled, empty-config, invalid-config, tool append,
default-policy fallback.
- All 109 tests pass across the three PLAN_EXEC test files.
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).