@@ -5595,6 +5595,112 @@ def policy_diff_cmd(
55955595 raise typer .Exit (3 )
55965596
55975597
5598+ @policy_app .command ("shadow-gate" )
5599+ def policy_shadow_gate_cmd (
5600+ trace : str = typer .Argument (
5601+ ...,
5602+ help = "Shadow trace file recorded by `tether serve --shadow-policy --record`." ,
5603+ ),
5604+ packet_dir : str = typer .Option (
5605+ "./shadow-rollout-packet" ,
5606+ "--packet-dir" ,
5607+ help = "Output packet directory for deployment-proof, policy-diff, and promotion decision artifacts." ,
5608+ ),
5609+ profile : str = typer .Option (
5610+ "lab-shadow" ,
5611+ "--profile" ,
5612+ help = "Promotion profile name or YAML/JSON path. Default: lab-shadow." ,
5613+ ),
5614+ candidate_active : bool = typer .Option (
5615+ False ,
5616+ "--candidate-active" ,
5617+ help = "Return ROLLBACK instead of HOLD when gates fail for an active candidate." ,
5618+ ),
5619+ min_compared : int = typer .Option (
5620+ 1 ,
5621+ "--min-compared" ,
5622+ help = "Minimum compared shadow requests required before a PROMOTE decision is allowed." ,
5623+ ),
5624+ wait_timeout_s : float = typer .Option (
5625+ 0.0 ,
5626+ "--wait-timeout-s" ,
5627+ help = "Seconds to wait for pending background shadow_result rows to flush." ,
5628+ ),
5629+ poll_s : float = typer .Option (
5630+ 0.25 ,
5631+ "--poll-s" ,
5632+ help = "Polling interval while waiting for shadow_result rows." ,
5633+ ),
5634+ fail_on : str = typer .Option (
5635+ "any" ,
5636+ "--fail-on" ,
5637+ help = "Policy diff gate: none/actions/latency/guard/shape/any." ,
5638+ ),
5639+ min_action_cos : float = typer .Option (
5640+ 0.995 ,
5641+ "--min-action-cos" ,
5642+ help = "Minimum cosine similarity before the action diff fails." ,
5643+ ),
5644+ max_action_delta : float = typer .Option (
5645+ 0.10 ,
5646+ "--max-action-delta" ,
5647+ help = "Max absolute action delta before the action diff fails." ,
5648+ ),
5649+ max_latency_regression_pct : float = typer .Option (
5650+ 0.10 ,
5651+ "--max-latency-regression-pct" ,
5652+ help = "Max shadow latency regression as a fraction, e.g. 0.10 = 10%." ,
5653+ ),
5654+ use_existing_packet : bool = typer .Option (
5655+ False ,
5656+ "--use-existing-packet" ,
5657+ help = "Use an existing deployment-proof.json in --packet-dir instead of writing a shadow-only proof packet." ,
5658+ ),
5659+ json_output : bool = typer .Option (
5660+ False ,
5661+ "--json" ,
5662+ help = "Print the full JSON report instead of a compact summary." ,
5663+ ),
5664+ ) -> None :
5665+ """Turn shadow trace evidence into PROMOTE, HOLD, or ROLLBACK."""
5666+ from tether .shadow_rollout import (
5667+ ShadowRolloutError ,
5668+ format_shadow_rollout_human ,
5669+ run_shadow_rollout_gate ,
5670+ )
5671+
5672+ try :
5673+ report = run_shadow_rollout_gate (
5674+ trace = trace ,
5675+ packet_dir = packet_dir ,
5676+ profile = profile ,
5677+ candidate_active = candidate_active ,
5678+ min_compared = min_compared ,
5679+ wait_timeout_s = wait_timeout_s ,
5680+ poll_s = poll_s ,
5681+ fail_on = fail_on ,
5682+ min_action_cos = min_action_cos ,
5683+ max_action_delta = max_action_delta ,
5684+ max_latency_regression_pct = max_latency_regression_pct ,
5685+ use_existing_packet = use_existing_packet ,
5686+ )
5687+ except ShadowRolloutError as exc :
5688+ err_console .print (f"[red]Shadow rollout gate failed:[/red] { exc } " )
5689+ raise typer .Exit (2 )
5690+
5691+ if json_output :
5692+ typer .echo (json .dumps (report , indent = 2 , sort_keys = True ))
5693+ else :
5694+ console .print (format_shadow_rollout_human (report ), markup = False )
5695+
5696+ decision = report .get ("decision" )
5697+ if decision == "PROMOTE" :
5698+ raise typer .Exit (0 )
5699+ if decision == "ROLLBACK" :
5700+ raise typer .Exit (4 )
5701+ raise typer .Exit (1 )
5702+
5703+
55985704app .add_typer (models_app , name = "models" )
55995705app .add_typer (train_app , name = "train" )
56005706app .add_typer (validate_app , name = "validate" )
0 commit comments