diff --git a/plugins/violin_guard/handlers/target_handlers.py b/plugins/violin_guard/handlers/target_handlers.py index 3ac96d5..b0ba22f 100644 --- a/plugins/violin_guard/handlers/target_handlers.py +++ b/plugins/violin_guard/handlers/target_handlers.py @@ -40,32 +40,18 @@ def handle_target(a, **kwargs): return _json("ok", value=value) -@_serialise_errors -def handle_status(a, **kwargs): - if not str(a.get("eng_dir") or "").strip(): - raise ValueError("eng_dir is required") - eng_dir = state.resolve_eng_dir(a.get("eng_dir", "")) - bootstrap_result = bootstrap.check_bootstrap(eng_dir, auto_repair=False) - tasks = ptt.parse_ptt(eng_dir / "state" / "ptt.md") - ptt_result = ptt.validate_ptt(tasks) - active = ptt.find_active_task(tasks) if not ptt_result.errors else None - current_phase = active.phase if active else None - pending = state.get_pending_sync(eng_dir) - credit_limit = int( - (pending or {}).get("credit_limit") or state.sync_credit_limit(current_phase) - ) - credit = state.sync_credit_remaining(eng_dir, current_phase) - counts = state.read_counts(eng_dir) - session_id = state.resolve_session_id(eng_dir) - marker = eng_dir / "state" / f".skill-loaded-{session_id}" if session_id else None - legacy_marker = str(marker) if marker and marker.is_file() else None - binding, binding_reason = ( - binding_readiness(eng_dir, task_id=active.id, session_id=session_id) - if active and session_id - else (None, "no active task or session") - ) - route = resolve_skill_route(current_phase or "RECON") - +def _evaluate_status_blockers( + eng_dir: Path, + bootstrap_result, + ptt_result, + active, + session_id: str | None, + binding_reason: str | None, + pending: dict | None, + credit: int, + current_phase: str | None, +) -> list[dict]: + """Evaluate and collect all current engagement workflow blockers.""" blockers = [ { "code": "bootstrap", @@ -125,8 +111,12 @@ def handle_status(a, **kwargs): "next_action": "Review engagement state, then call violin_heartbeat_done", } ) + return blockers - phase_requirements = { + +def _build_phase_requirements() -> dict: + """Build requirement mapping for all phases.""" + return { phase.value: { "ptt_phase": "EXPLOITATION" if phase is Phase.POST_EXPLOITATION else phase.value, "hypothesis_required": requires_hypothesis(phase), @@ -135,10 +125,75 @@ def handle_status(a, **kwargs): } for phase in Phase } + + +def _build_status_skill_summary( + session_id: str | None, + binding, + binding_reason: str | None, + route, + legacy_marker: str | None, +) -> dict: + """Format skill binding and route summary dictionary.""" + return { + "session_id": session_id or None, + "binding": binding, + "binding_ready": binding_reason is None, + "binding_reason": binding_reason, + "route_candidates": list(route.allowed), + "legacy_marker": legacy_marker, + "legacy_marker_status": "obsolete" if legacy_marker else "absent", + "recovery": ( + "Select a route candidate with violin_record_ptt; repeat after the preparation " + "result returns to the model" + ), + } + + +@_serialise_errors +def handle_status(a, **kwargs): + if not str(a.get("eng_dir") or "").strip(): + raise ValueError("eng_dir is required") + eng_dir = state.resolve_eng_dir(a.get("eng_dir", "")) + bootstrap_result = bootstrap.check_bootstrap(eng_dir, auto_repair=False) + tasks = ptt.parse_ptt(eng_dir / "state" / "ptt.md") + ptt_result = ptt.validate_ptt(tasks) + active = ptt.find_active_task(tasks) if not ptt_result.errors else None + current_phase = active.phase if active else None + pending = state.get_pending_sync(eng_dir) + credit_limit = int( + (pending or {}).get("credit_limit") or state.sync_credit_limit(current_phase) + ) + credit = state.sync_credit_remaining(eng_dir, current_phase) + counts = state.read_counts(eng_dir) + session_id = state.resolve_session_id(eng_dir) + marker = eng_dir / "state" / f".skill-loaded-{session_id}" if session_id else None + legacy_marker = str(marker) if marker and marker.is_file() else None + binding, binding_reason = ( + binding_readiness(eng_dir, task_id=active.id, session_id=session_id) + if active and session_id + else (None, "no active task or session") + ) + route = resolve_skill_route(current_phase or "RECON") + + blockers = _evaluate_status_blockers( + eng_dir, + bootstrap_result, + ptt_result, + active, + session_id, + binding_reason, + pending, + credit, + current_phase, + ) + phase_requirements = _build_phase_requirements() pending_commands = [ {"command": item.get("command", ""), "required_phase": item.get("phase", "")} for item in (pending or {}).get("commands") or [] ] + heartbeat_pending = state.has_heartbeat_pending(eng_dir) + return _json( "blocked" if blockers else "ok", engagement=str(eng_dir), @@ -165,18 +220,8 @@ def handle_status(a, **kwargs): heartbeat_reason=state.get_heartbeat_reason(eng_dir), command_count=counts["commands"], message_count=counts["messages"], - skill={ - "session_id": session_id or None, - "binding": binding, - "binding_ready": binding_reason is None, - "binding_reason": binding_reason, - "route_candidates": list(route.allowed), - "legacy_marker": legacy_marker, - "legacy_marker_status": "obsolete" if legacy_marker else "absent", - "recovery": ( - "Select a route candidate with violin_record_ptt; repeat after the preparation " - "result returns to the model" - ), - }, + skill=_build_status_skill_summary( + session_id, binding, binding_reason, route, legacy_marker + ), runtime=runtime_backend.runtime_readiness(eng_dir), )