refactorai-cli 0.7.4__tar.gz → 0.7.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/PKG-INFO +2 -2
  2. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/pyproject.toml +2 -2
  3. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/__init__.py +1 -1
  4. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/run_cmds.py +232 -69
  5. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/watch_cmds.py +39 -11
  6. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli.egg-info/PKG-INFO +2 -2
  7. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli.egg-info/requires.txt +1 -1
  8. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/README.md +0 -0
  9. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/auth.py +0 -0
  10. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/client.py +0 -0
  11. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/cloud_rr.py +0 -0
  12. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/__init__.py +0 -0
  13. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/account_cmds.py +0 -0
  14. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/auth_cmds.py +0 -0
  15. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/branch_cmds.py +0 -0
  16. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/cloud_cmds.py +0 -0
  17. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/engine_cmds.py +0 -0
  18. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/hook_cmds.py +0 -0
  19. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/model_cmds.py +0 -0
  20. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/policy_cmds.py +0 -0
  21. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/pre_push_cmds.py +0 -0
  22. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/request_cmds.py +0 -0
  23. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/rules_cmds.py +0 -0
  24. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/runtime_cmds.py +0 -0
  25. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
  26. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/setup_cmds.py +0 -0
  27. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/toolchains_cmds.py +0 -0
  28. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/workspace_cmds.py +0 -0
  29. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commit_queue.py +0 -0
  30. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commit_telemetry.py +0 -0
  31. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/control_plane.py +0 -0
  32. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/credentials.py +0 -0
  33. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/dotenv_loader.py +0 -0
  34. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/git_scope.py +0 -0
  35. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/local_constitution.py +0 -0
  36. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/local_engine_runtime.py +0 -0
  37. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/local_paths.py +0 -0
  38. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/main.py +0 -0
  39. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/model_policy.py +0 -0
  40. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/pre_push_gate.py +0 -0
  41. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/refactor_branch_store.py +0 -0
  42. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/review_runner.py +0 -0
  43. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/runtime_manager.py +0 -0
  44. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/settings.py +0 -0
  45. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/setup_flow.py +0 -0
  46. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/watch_ledger.py +0 -0
  47. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/watch_state.py +0 -0
  48. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/watch_supervisor.py +0 -0
  49. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli.egg-info/SOURCES.txt +0 -0
  50. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli.egg-info/dependency_links.txt +0 -0
  51. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli.egg-info/entry_points.txt +0 -0
  52. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli.egg-info/top_level.txt +0 -0
  53. {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.7.4
3
+ Version: 0.7.6
4
4
  Summary: Local-first CLI for the refactor platform
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -8,7 +8,7 @@ Requires-Dist: typer>=0.12.0
8
8
  Requires-Dist: httpx>=0.27.0
9
9
  Requires-Dist: rich>=13.7.0
10
10
  Requires-Dist: PyYAML>=6.0.1
11
- Requires-Dist: refactorai-core>=3.2.16
11
+ Requires-Dist: refactorai-core>=3.2.18
12
12
 
13
13
  # refactorai-cli
14
14
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "refactorai-cli"
3
- version = "0.7.4"
3
+ version = "0.7.6"
4
4
  description = "Local-first CLI for the refactor platform"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -12,7 +12,7 @@ dependencies = [
12
12
  "httpx>=0.27.0",
13
13
  "rich>=13.7.0",
14
14
  "PyYAML>=6.0.1",
15
- "refactorai-core>=3.2.16",
15
+ "refactorai-core>=3.2.18",
16
16
  ]
17
17
 
18
18
  [project.scripts]
@@ -5,4 +5,4 @@ the shared `refactor_core` pipeline from a project folder while staying
5
5
  authenticated to the hosted platform via a developer key.
6
6
  """
7
7
 
8
- __version__ = "0.7.4"
8
+ __version__ = "0.7.6"
@@ -15,6 +15,7 @@ import shlex
15
15
  import shutil
16
16
  import subprocess
17
17
  import sys
18
+ import time
18
19
  import uuid
19
20
  from pathlib import Path
20
21
  from typing import Callable
@@ -57,7 +58,7 @@ from refactor_core.apply import (
57
58
  read_marker,
58
59
  revert_from_backup,
59
60
  )
60
- from refactor_core.budgeting import compute_budget
61
+ from refactor_core.budgeting import DEFAULT_BUDGET_RATIO, compute_budget, plan_batches
61
62
  from refactor_core.capabilities import resolve_capabilities
62
63
  from refactor_core.gate import evaluate_gate
63
64
  from refactor_core.merge_gate import (
@@ -692,7 +693,6 @@ def _stable_idempotency_key(
692
693
  stage: str,
693
694
  constitution_hash: str,
694
695
  target: str,
695
- max_alerts: int,
696
696
  execution_mode: str,
697
697
  scope_digest: str = "",
698
698
  ) -> str:
@@ -714,13 +714,129 @@ def _stable_idempotency_key(
714
714
  stage,
715
715
  constitution_hash,
716
716
  str(target),
717
- str(int(max_alerts)),
718
717
  scope_digest,
719
718
  ]
720
719
  )
721
720
  return hashlib.sha256(seed.encode("utf-8")).hexdigest()
722
721
 
723
722
 
723
+ class _CloudCapabilityShim:
724
+ """Minimal provider-like object for cloud batch sizing.
725
+
726
+ ``plan_batches`` needs a resolved ``CapabilitySnapshot`` (context window +
727
+ output ceiling) to pack documents. On the cloud path there is no local
728
+ provider instance, so this shim carries just the provider/model identity;
729
+ :func:`resolve_capabilities` fills in the catalog-derived context window.
730
+ """
731
+
732
+ supports_tools = False
733
+ supports_refactor = True
734
+
735
+ def __init__(self, provider_name: str, model_id: str) -> None:
736
+ self.provider_name = provider_name
737
+ self.model_id = model_id
738
+
739
+
740
+ def _plan_cloud_batches(constitution, documents: list[dict], *, model_hint: str) -> list[list[dict]]:
741
+ """Split indexed documents into context-safe batches for cloud inference.
742
+
743
+ Mirrors the local planner so a large first review is sent as several bounded
744
+ requests instead of one oversized call that blows the read timeout. Always
745
+ returns at least one batch (possibly empty) so callers can loop uniformly.
746
+ """
747
+ provider_name = str(constitution.get_setting("provider", "") or "").strip() or "cloud"
748
+ settings = dict(getattr(constitution, "settings", {}) or {})
749
+ ratio = float(
750
+ constitution.get_setting("context_budget_ratio", DEFAULT_BUDGET_RATIO) or DEFAULT_BUDGET_RATIO
751
+ )
752
+ capabilities = resolve_capabilities(_CloudCapabilityShim(provider_name, model_hint), settings)
753
+ plan = plan_batches(documents, capabilities, ratio=ratio)
754
+ return plan.batches or [list(documents)]
755
+
756
+
757
+ def _cloud_timeout(constitution) -> httpx.Timeout:
758
+ """Split connect/read timeout for cloud calls (configurable via refactor.config).
759
+
760
+ A single long ``timeout=`` collapses connect and read into one number; a
761
+ first-run review needs a generous *read* budget (the model is thinking) but a
762
+ short *connect* budget (fail fast on an unreachable host).
763
+ """
764
+ read = float(constitution.get_setting("cloud_request_timeout", 120) or 120)
765
+ connect = float(constitution.get_setting("cloud_connect_timeout", 10) or 10)
766
+ return httpx.Timeout(read, connect=connect, write=read, pool=connect)
767
+
768
+
769
+ def _cloud_max_retries(constitution) -> int:
770
+ """Bounded retry count for transient cloud failures (default 2 => 3 attempts)."""
771
+ raw = constitution.get_setting("cloud_max_retries", 2)
772
+ try:
773
+ return max(0, int(raw))
774
+ except (TypeError, ValueError):
775
+ return 2
776
+
777
+
778
+ _RETRYABLE_INFERENCE_STATUS = {429, 500, 502, 503, 504}
779
+
780
+
781
+ def _inference_backoff_seconds(attempt: int) -> float:
782
+ """Linear backoff capped at 8s (attempt is 1-based)."""
783
+ return min(2.0 * attempt, 8.0)
784
+
785
+
786
+ def _post_cloud_inference(
787
+ url: str,
788
+ *,
789
+ headers: dict,
790
+ payload: dict,
791
+ timeout: httpx.Timeout,
792
+ max_retries: int,
793
+ ) -> httpx.Response:
794
+ """POST one inference request, retrying transient timeouts/5xx/429.
795
+
796
+ Retries are safe because every request carries a stable ``Idempotency-Key``
797
+ (the backend replays instead of re-charging), so a read timeout on a slow
798
+ first run recovers without duplicating work.
799
+ """
800
+ attempt = 0
801
+ while True:
802
+ try:
803
+ response = httpx.post(url, headers=headers, json=payload, timeout=timeout)
804
+ except httpx.HTTPError as exc:
805
+ retryable = isinstance(exc, (httpx.TimeoutException, httpx.TransportError))
806
+ if retryable and attempt < max_retries:
807
+ attempt += 1
808
+ time.sleep(_inference_backoff_seconds(attempt))
809
+ continue
810
+ raise RuntimeError(f"Could not reach cloud inference service: {exc}") from exc
811
+ if response.status_code in _RETRYABLE_INFERENCE_STATUS and attempt < max_retries:
812
+ attempt += 1
813
+ time.sleep(_inference_backoff_seconds(attempt))
814
+ continue
815
+ return response
816
+
817
+
818
+ def _raise_for_inference_status(response: httpx.Response, execution_mode: str) -> None:
819
+ """Map a non-2xx inference response to an actionable ``RuntimeError``."""
820
+ if response.status_code == 401:
821
+ raise RuntimeError("Cloud inference unauthorized (developer key invalid or revoked).")
822
+ if response.status_code == 402:
823
+ if execution_mode == "cloud_byok":
824
+ raise RuntimeError(
825
+ "Cloud BYOK is not enabled for this account (missing `run:byok`). "
826
+ "Contact your admin to enable BYOK cloud execution."
827
+ )
828
+ raise RuntimeError("Cloud inference requires paid tier (`mode: cloud_managed`).")
829
+ if response.status_code == 404 and execution_mode == "cloud_byok":
830
+ raise RuntimeError(
831
+ "BYOK credential not found or not owned by this account. "
832
+ "Check `refactor cloud credentials list` and your `credential_ref`."
833
+ )
834
+ if response.status_code == 429:
835
+ raise RuntimeError("Cloud inference quota exhausted or rate-limited.")
836
+ if response.status_code >= 400:
837
+ raise RuntimeError(f"Cloud inference failed ({response.status_code}): {response.text[:500]}")
838
+
839
+
724
840
  def _cloud_inference(
725
841
  *,
726
842
  stage: str,
@@ -731,8 +847,9 @@ def _cloud_inference(
731
847
  findings: list | None = None,
732
848
  scope_paths: list[str] | None = None,
733
849
  ) -> RunArtifact:
734
- _raw_alerts = constitution.get_setting("max_alerts_per_run", 5)
735
- max_alerts = int(_raw_alerts if _raw_alerts is not None else 5)
850
+ # Cloud inference is intentionally uncapped on the client: send/accept the
851
+ # full model output instead of truncating findings/patches/tests.
852
+ max_alerts = 0
736
853
  gate_mode = str(constitution.get_setting("gate_mode", "strict") or "strict")
737
854
  excludes = set(constitution.get_setting("exclude", []) or [])
738
855
  # Commit-scoped cloud review (docs/41 `R28`): when a scope is supplied, index
@@ -756,77 +873,123 @@ def _cloud_inference(
756
873
 
757
874
  execution_mode = _cloud_execution_mode(constitution) or "cloud_managed"
758
875
  auth_ctx = ensure_authenticated(project_root)
759
- # Digest the exact documents so the idempotency key keys on the reviewed
760
- # code, not just account/stage/target. (The backend independently recomputes
761
- # a documents hash for its own replay guard, so this need not be sent.)
762
- documents_hash = _documents_digest(documents)
763
- payload = {
764
- "stage": stage,
765
- "execution_mode": execution_mode,
766
- "constitution_hash": constitution.content_hash,
767
- "documents": documents,
768
- "constitution_body": getattr(constitution, "body", "") or "",
769
- "findings": request_findings if stage != "review" else [],
770
- "max_alerts": max_alerts,
771
- "model_hint": model_hint or str(constitution.get_setting("model_id", "managed-default") or "managed-default"),
772
- }
876
+ model_hint_value = model_hint or str(
877
+ constitution.get_setting("model_id", "managed-default") or "managed-default"
878
+ )
773
879
 
774
- if execution_mode == "cloud_byok":
775
- _apply_byok_payload(payload, constitution)
880
+ # Split the indexed documents into context-safe batches so a large first
881
+ # review is never sent as one oversized request that blows the read timeout.
882
+ # Each batch is an independent, idempotent inference call; findings/patches/
883
+ # tests are aggregated client-side (mirrors the local planner). Each call
884
+ # uses a split connect/read timeout and bounded retries for transient
885
+ # timeouts/5xx/429 (safe because of the stable per-batch idempotency key).
886
+ batches = _plan_cloud_batches(constitution, documents, model_hint=model_hint_value)
887
+ timeout = _cloud_timeout(constitution)
888
+ max_retries = _cloud_max_retries(constitution)
889
+ url = f"{platform_url()}/v1/inference/refactor"
890
+ batch_count = len(batches)
891
+ if batch_count > 1:
892
+ console.print(
893
+ f"[dim]cloud {stage}: {len(documents)} document(s) split into "
894
+ f"{batch_count} batch(es).[/dim]"
895
+ )
776
896
 
777
- idempotency_key = _stable_idempotency_key(
778
- account_id=auth_ctx.account_id,
779
- stage=stage,
780
- constitution_hash=constitution.content_hash,
781
- target=target,
782
- max_alerts=max_alerts,
783
- execution_mode=execution_mode,
784
- scope_digest=documents_hash,
785
- )
786
- try:
787
- response = httpx.post(
788
- f"{platform_url()}/v1/inference/refactor",
897
+ findings: list[Finding] = []
898
+ patches: list[PatchGroup] = []
899
+ generated_tests: list[GeneratedTest] = []
900
+ seen_findings: set[tuple] = set()
901
+ resolved_model_id = ""
902
+ server_security_summary: dict = {}
903
+
904
+ for idx, batch in enumerate(batches, start=1):
905
+ batch_paths = {str(d.get("path", "")) for d in batch}
906
+ # For the refactor stage, forward only the prior findings relevant to
907
+ # this batch's files (fall back to all when none match), so each request
908
+ # stays scoped and small (parity with the local planner's per-batch set).
909
+ if stage != "review":
910
+ batch_findings_payload = [
911
+ f for f in request_findings
912
+ if str((f or {}).get("file_path", "")) in batch_paths
913
+ ] or request_findings
914
+ else:
915
+ batch_findings_payload = []
916
+
917
+ payload = {
918
+ "stage": stage,
919
+ "execution_mode": execution_mode,
920
+ "constitution_hash": constitution.content_hash,
921
+ "documents": batch,
922
+ "constitution_body": getattr(constitution, "body", "") or "",
923
+ "findings": batch_findings_payload,
924
+ "max_alerts": max_alerts,
925
+ "model_hint": model_hint_value,
926
+ }
927
+ if execution_mode == "cloud_byok":
928
+ _apply_byok_payload(payload, constitution)
929
+
930
+ # Digest the exact batch documents (plus batch index/count) so the
931
+ # idempotency key keys on the reviewed code: a genuine retry replays, but
932
+ # distinct batches never collide onto one cached result.
933
+ batch_digest = _documents_digest(batch)
934
+ idempotency_key = _stable_idempotency_key(
935
+ account_id=auth_ctx.account_id,
936
+ stage=stage,
937
+ constitution_hash=constitution.content_hash,
938
+ target=target,
939
+ execution_mode=execution_mode,
940
+ scope_digest=f"{batch_digest}:{idx}:{batch_count}",
941
+ )
942
+ if batch_count > 1:
943
+ console.print(
944
+ f"[dim]cloud {stage}: batch {idx}/{batch_count} "
945
+ f"({len(batch)} document(s))...[/dim]"
946
+ )
947
+
948
+ response = _post_cloud_inference(
949
+ url,
789
950
  headers={
790
951
  "Authorization": f"Bearer {auth_ctx.key.key}",
791
952
  "Content-Type": "application/json",
792
953
  "Idempotency-Key": idempotency_key,
793
954
  },
794
- json=payload,
795
- timeout=45.0,
955
+ payload=payload,
956
+ timeout=timeout,
957
+ max_retries=max_retries,
796
958
  )
797
- except httpx.HTTPError as exc:
798
- raise RuntimeError(f"Could not reach cloud inference service: {exc}") from exc
799
-
800
- if response.status_code == 401:
801
- raise RuntimeError("Cloud inference unauthorized (developer key invalid or revoked).")
802
- if response.status_code == 402:
803
- if execution_mode == "cloud_byok":
804
- raise RuntimeError(
805
- "Cloud BYOK is not enabled for this account (missing `run:byok`). "
806
- "Contact your admin to enable BYOK cloud execution."
959
+ _raise_for_inference_status(response, execution_mode)
960
+
961
+ data = response.json() if response.content else {}
962
+ for item in (data.get("findings") or []):
963
+ finding = Finding.model_validate(item)
964
+ key = (
965
+ finding.severity,
966
+ finding.category,
967
+ finding.file_path,
968
+ finding.symbol,
969
+ finding.rationale,
807
970
  )
808
- raise RuntimeError("Cloud inference requires paid tier (`mode: cloud_managed`).")
809
- if response.status_code == 404 and execution_mode == "cloud_byok":
810
- raise RuntimeError(
811
- "BYOK credential not found or not owned by this account. "
812
- "Check `refactor cloud credentials list` and your `credential_ref`."
813
- )
814
- if response.status_code == 429:
815
- raise RuntimeError("Cloud inference quota exhausted or rate-limited.")
816
- if response.status_code >= 400:
817
- raise RuntimeError(f"Cloud inference failed ({response.status_code}): {response.text[:500]}")
818
-
819
- data = response.json() if response.content else {}
820
- _no_cap = max_alerts <= 0
821
- findings = [Finding.model_validate(item) for item in (data.get("findings") or [])]
822
- patches = [PatchGroup.model_validate(item) for item in (data.get("patches") or [])]
823
- generated_tests = [
824
- GeneratedTest.model_validate(item) for item in (data.get("generated_tests") or [])
825
- ]
826
- if not _no_cap:
827
- findings = findings[:max_alerts]
828
- patches = patches[:max_alerts]
829
- generated_tests = generated_tests[:max_alerts]
971
+ if key in seen_findings:
972
+ continue
973
+ seen_findings.add(key)
974
+ findings.append(finding)
975
+ for item in (data.get("patches") or []):
976
+ patches.append(PatchGroup.model_validate(item))
977
+ for item in (data.get("generated_tests") or []):
978
+ generated_tests.append(GeneratedTest.model_validate(item))
979
+ if not resolved_model_id:
980
+ resolved_model_id = str(data.get("model_id") or "")
981
+ # Prefer the first server-provided security baseline across batches.
982
+ batch_server_summary = data.get("security_summary")
983
+ if (
984
+ not server_security_summary
985
+ and isinstance(batch_server_summary, dict)
986
+ and batch_server_summary.get("enabled")
987
+ ):
988
+ server_security_summary = batch_server_summary
989
+
990
+ # Stable ordering across batches: highest severity first.
991
+ severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
992
+ findings.sort(key=lambda f: severity_order.get(f.severity, 4))
830
993
  gate = evaluate_gate(findings, [], mode=gate_mode)
831
994
 
832
995
  # Optional security-scan enrichment (R19). Semgrep is an additive signal and
@@ -836,7 +999,7 @@ def _cloud_inference(
836
999
  # the local checkout (Phase 1) so parity holds against older backends.
837
1000
  security_summary: dict = {}
838
1001
  security_notes: list[str] = []
839
- server_summary = data.get("security_summary")
1002
+ server_summary = server_security_summary
840
1003
  if isinstance(server_summary, dict) and server_summary.get("enabled"):
841
1004
  security_summary = server_summary
842
1005
  else:
@@ -870,7 +1033,7 @@ def _cloud_inference(
870
1033
  gate=gate,
871
1034
  summary=summary,
872
1035
  constitution_hash=constitution.content_hash,
873
- model_id=str(data.get("model_id") or payload["model_hint"]),
1036
+ model_id=str(resolved_model_id or model_hint_value),
874
1037
  notes=security_notes,
875
1038
  rule_source=rule_meta.get("rule_source", ""),
876
1039
  rule_pattern=rule_meta.get("rule_pattern", ""),
@@ -489,22 +489,40 @@ def process_pending(
489
489
  return reviewed
490
490
 
491
491
 
492
- def _read_key(timeout: float) -> str:
493
- """Non-blocking key read on a POSIX TTY, normalized to a token.
494
-
495
- Returns ``up``/``down`` for arrow keys, the raw character for normal keys
496
- (used for hotkeys like ``[r]`` and ``[f]``), ``esc`` for a
497
- bare Escape, or ``""`` when nothing was pressed / the terminal is not
498
- interactive. Sleeps ``timeout`` in the non-interactive case so the caller's
499
- poll cadence is preserved.
500
- """
501
- if not sys.stdin.isatty():
492
+ def _read_key_windows(timeout: float) -> str:
493
+ """Non-blocking key read on Windows consoles (PowerShell/cmd/Terminal)."""
494
+ try:
495
+ import msvcrt
496
+ except ImportError:
502
497
  time.sleep(timeout)
503
498
  return ""
499
+ deadline = time.monotonic() + max(0.0, float(timeout))
500
+ while True:
501
+ if msvcrt.kbhit():
502
+ ch = msvcrt.getwch()
503
+ # Windows emits extended keys as a two-char sequence:
504
+ # prefix (\x00 or \xe0) + code (H/P/K/M etc).
505
+ if ch in ("\x00", "\xe0"):
506
+ code = msvcrt.getwch() if msvcrt.kbhit() else ""
507
+ if code == "H":
508
+ return "up"
509
+ if code == "P":
510
+ return "down"
511
+ return ""
512
+ if ch == "\x1b":
513
+ return "esc"
514
+ return ch
515
+ if time.monotonic() >= deadline:
516
+ return ""
517
+ time.sleep(0.01)
518
+
519
+
520
+ def _read_key_posix(timeout: float) -> str:
521
+ """Non-blocking key read on a POSIX TTY, normalized to a token."""
504
522
  try:
505
523
  import termios
506
524
  import tty
507
- except ImportError: # non-POSIX
525
+ except ImportError:
508
526
  time.sleep(timeout)
509
527
  return ""
510
528
  fd = sys.stdin.fileno()
@@ -534,6 +552,16 @@ def _read_key(timeout: float) -> str:
534
552
  termios.tcsetattr(fd, termios.TCSADRAIN, old)
535
553
 
536
554
 
555
+ def _read_key(timeout: float) -> str:
556
+ """Cross-platform non-blocking key read for ``refactor watch`` hotkeys."""
557
+ if not sys.stdin.isatty():
558
+ time.sleep(timeout)
559
+ return ""
560
+ if os.name == "nt":
561
+ return _read_key_windows(timeout)
562
+ return _read_key_posix(timeout)
563
+
564
+
537
565
  def build_action_command(
538
566
  action: str,
539
567
  sha: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.7.4
3
+ Version: 0.7.6
4
4
  Summary: Local-first CLI for the refactor platform
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -8,7 +8,7 @@ Requires-Dist: typer>=0.12.0
8
8
  Requires-Dist: httpx>=0.27.0
9
9
  Requires-Dist: rich>=13.7.0
10
10
  Requires-Dist: PyYAML>=6.0.1
11
- Requires-Dist: refactorai-core>=3.2.16
11
+ Requires-Dist: refactorai-core>=3.2.18
12
12
 
13
13
  # refactorai-cli
14
14
 
@@ -2,4 +2,4 @@ typer>=0.12.0
2
2
  httpx>=0.27.0
3
3
  rich>=13.7.0
4
4
  PyYAML>=6.0.1
5
- refactorai-core>=3.2.16
5
+ refactorai-core>=3.2.18
File without changes
File without changes