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.
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/PKG-INFO +2 -2
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/pyproject.toml +2 -2
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/__init__.py +1 -1
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/run_cmds.py +232 -69
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/watch_cmds.py +39 -11
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli.egg-info/PKG-INFO +2 -2
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli.egg-info/requires.txt +1 -1
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/README.md +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/auth.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/client.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/cloud_rr.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/__init__.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/account_cmds.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/auth_cmds.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/branch_cmds.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/cloud_cmds.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/engine_cmds.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/hook_cmds.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/model_cmds.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/policy_cmds.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/pre_push_cmds.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/request_cmds.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/rules_cmds.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/runtime_cmds.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/setup_cmds.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/toolchains_cmds.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commands/workspace_cmds.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commit_queue.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/commit_telemetry.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/control_plane.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/credentials.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/dotenv_loader.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/git_scope.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/local_constitution.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/local_engine_runtime.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/local_paths.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/main.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/model_policy.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/pre_push_gate.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/refactor_branch_store.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/review_runner.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/runtime_manager.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/settings.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/setup_flow.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/watch_ledger.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/watch_state.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli/watch_supervisor.py +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli.egg-info/SOURCES.txt +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli.egg-info/dependency_links.txt +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli.egg-info/entry_points.txt +0 -0
- {refactorai_cli-0.7.4 → refactorai_cli-0.7.6}/refactorai_cli.egg-info/top_level.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
15
|
+
"refactorai-core>=3.2.18",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
[project.scripts]
|
|
@@ -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
|
-
|
|
735
|
-
|
|
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
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
-
|
|
775
|
-
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
795
|
-
timeout=
|
|
955
|
+
payload=payload,
|
|
956
|
+
timeout=timeout,
|
|
957
|
+
max_retries=max_retries,
|
|
796
958
|
)
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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 =
|
|
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(
|
|
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
|
|
493
|
-
"""Non-blocking key read on
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
11
|
+
Requires-Dist: refactorai-core>=3.2.18
|
|
12
12
|
|
|
13
13
|
# refactorai-cli
|
|
14
14
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|