refactorai-cli 0.3.2__tar.gz → 0.3.4__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 (34) hide show
  1. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/PKG-INFO +3 -3
  2. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/README.md +1 -1
  3. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/pyproject.toml +2 -2
  4. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/__init__.py +1 -1
  5. refactorai_cli-0.3.4/refactorai_cli/cloud_rr.py +730 -0
  6. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/commands/run_cmds.py +44 -187
  7. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli.egg-info/PKG-INFO +3 -3
  8. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli.egg-info/SOURCES.txt +1 -0
  9. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli.egg-info/requires.txt +1 -1
  10. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/auth.py +0 -0
  11. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/client.py +0 -0
  12. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/commands/__init__.py +0 -0
  13. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/commands/auth_cmds.py +0 -0
  14. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/commands/cloud_cmds.py +0 -0
  15. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/commands/engine_cmds.py +0 -0
  16. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/commands/model_cmds.py +0 -0
  17. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/commands/rules_cmds.py +0 -0
  18. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/commands/runtime_cmds.py +0 -0
  19. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
  20. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/commands/setup_cmds.py +0 -0
  21. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/control_plane.py +0 -0
  22. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/credentials.py +0 -0
  23. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/local_constitution.py +0 -0
  24. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/local_engine_runtime.py +0 -0
  25. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/local_paths.py +0 -0
  26. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/main.py +0 -0
  27. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/model_policy.py +0 -0
  28. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/runtime_manager.py +0 -0
  29. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/settings.py +0 -0
  30. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli/setup_flow.py +0 -0
  31. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli.egg-info/dependency_links.txt +0 -0
  32. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli.egg-info/entry_points.txt +0 -0
  33. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/refactorai_cli.egg-info/top_level.txt +0 -0
  34. {refactorai_cli-0.3.2 → refactorai_cli-0.3.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.3.2
3
+ Version: 0.3.4
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.0.2
11
+ Requires-Dist: refactorai-core>=3.0.4
12
12
 
13
13
  # refactorai-cli
14
14
 
@@ -54,7 +54,7 @@ python -m twine upload ./refactorai-cli/dist/*
54
54
  ## Install test (local)
55
55
 
56
56
  ```bash
57
- python -m pip install ./refactorai-cli/dist/refactorai_cli-0.3.2-py3-none-any.whl
57
+ python -m pip install ./refactorai-cli/dist/refactorai_cli-0.3.4-py3-none-any.whl
58
58
  refactor --version
59
59
  ```
60
60
 
@@ -42,7 +42,7 @@ python -m twine upload ./refactorai-cli/dist/*
42
42
  ## Install test (local)
43
43
 
44
44
  ```bash
45
- python -m pip install ./refactorai-cli/dist/refactorai_cli-0.3.2-py3-none-any.whl
45
+ python -m pip install ./refactorai-cli/dist/refactorai_cli-0.3.4-py3-none-any.whl
46
46
  refactor --version
47
47
  ```
48
48
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "refactorai-cli"
3
- version = "0.3.2"
3
+ version = "0.3.4"
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.0.2",
15
+ "refactorai-core>=3.0.4",
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.3.2"
8
+ __version__ = "0.3.4"
@@ -0,0 +1,730 @@
1
+ """Client-side orchestrator for the cloud server-run per-RR loop (R23; doc 35).
2
+
3
+ This module is the thin "hand" in the Cursor-style split: the proprietary
4
+ per-RR decision loop runs on the platform (see ``backend/app/api/v1/rr_session``),
5
+ and this client executes only generic primitives and reports results:
6
+
7
+ - open a session with the pending RRs' context,
8
+ - write the server-generated characterization test and run it (baseline),
9
+ - render the server proposal and collect approval (per RR, mid-run),
10
+ - apply the approved diff, run the test again, report the result,
11
+ - honor the server verdict (applied / retry+revert / denied / deferred / stop),
12
+ - persist RR status + build the run summary.
13
+
14
+ IMPORTANT (IP + packaging boundary): this module MUST import only client-safe
15
+ ``refactor_core`` modules (``apply``, ``refactor_requests``, ``models``,
16
+ ``constitution``, ``store``, ``testcmd``). It MUST NOT import any engine-only
17
+ subpackage (``execution``, ``codeintel``, ``enginecore``, ``providers``); doing
18
+ so would both leak proprietary code to pip-only installs and re-introduce the
19
+ ``ModuleNotFoundError: No module named 'refactor_core.execution'`` crash that
20
+ this design fixes. ``tests/test_cloud_rr_client_boundary.py`` enforces this.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import hashlib
26
+ import re
27
+ import shlex
28
+ import shutil
29
+ import subprocess
30
+ from pathlib import Path
31
+ from typing import Callable
32
+
33
+ import httpx
34
+
35
+ # Client-safe core modules only (see module docstring).
36
+ from refactor_core.apply import (
37
+ apply_changes,
38
+ compute_file_changes,
39
+ revert_from_backup,
40
+ validate_diff,
41
+ )
42
+ from refactor_core.models import GateResult, RunArtifact
43
+ from refactor_core.refactor_requests import (
44
+ APPLIED,
45
+ DEFERRED,
46
+ DENIED,
47
+ FAILED,
48
+ IN_PROGRESS,
49
+ pending_requests,
50
+ rr_test_dir,
51
+ save_request,
52
+ )
53
+ from refactor_core.store import create_run
54
+ from refactor_core.testcmd import _SKIP_DIRS, discover_test_command, resolve_test_command
55
+
56
+ from refactorai_cli.auth import ensure_authenticated
57
+ from refactorai_cli.settings import platform_url
58
+
59
+ _ENV_ASSIGN_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=.*$")
60
+
61
+ # Manifest/config files that reveal how a project runs its tests. Sent (truncated)
62
+ # to the server for agentic test-command discovery when deterministic detection
63
+ # finds nothing.
64
+ _MANIFEST_NAMES = (
65
+ "package.json",
66
+ "pyproject.toml",
67
+ "setup.cfg",
68
+ "setup.py",
69
+ "pytest.ini",
70
+ "tox.ini",
71
+ "noxfile.py",
72
+ "requirements.txt",
73
+ "Makefile",
74
+ "makefile",
75
+ "go.mod",
76
+ "Cargo.toml",
77
+ "build.gradle",
78
+ "pom.xml",
79
+ )
80
+
81
+
82
+ class CloudRRError(RuntimeError):
83
+ """Raised when the cloud RR session cannot proceed."""
84
+
85
+
86
+ def _read_source(project_root: Path, rel: str) -> str:
87
+ path = project_root / rel
88
+ if not path.is_file():
89
+ return ""
90
+ try:
91
+ return path.read_text(encoding="utf-8")
92
+ except OSError:
93
+ return ""
94
+
95
+
96
+ def _run_test(project_root: Path, test_command: str | None) -> tuple[bool, str]:
97
+ """Run the configured test command on the host; (passed, detail).
98
+
99
+ Cloud runs execute tests client-side because the developer's code + deps live
100
+ here. Behavior preservation (FR3/FR3a) requires a runnable test: if NO test
101
+ command is configured we return ``(False, ...)`` so the server defers the RR
102
+ rather than applying an unverifiable change. This differs from local's
103
+ permissive skip on purpose -- cloud never applies an unverified change.
104
+ """
105
+ if not test_command or not str(test_command).strip():
106
+ return False, "no test command configured; cannot verify behavior preservation"
107
+ command = str(test_command).strip()
108
+ if command == "npm test":
109
+ command = "CI=true npm test -- --watch=false"
110
+ try:
111
+ proc = subprocess.run(
112
+ command,
113
+ shell=True,
114
+ cwd=str(project_root),
115
+ capture_output=True,
116
+ text=True,
117
+ timeout=600,
118
+ )
119
+ except (subprocess.SubprocessError, OSError) as exc:
120
+ return False, f"test command error: {exc}"
121
+ if proc.returncode == 0:
122
+ return True, "tests passed"
123
+ return False, (proc.stdout or proc.stderr or "")[-500:]
124
+
125
+
126
+ def _client_preflight_command(project_root: Path, command: str | None) -> bool:
127
+ """Return True when ``command`` looks runnable on this host.
128
+
129
+ Composite/shell commands (``&&``, subshells, pipes) run under ``shell=True``
130
+ and can't be reduced to one binary, so we trust them and let the real exit
131
+ code gate. Simple commands are validated by checking the leading binary is on
132
+ PATH — this guards against running (and charging for) a server-suggested
133
+ command whose toolchain isn't installed.
134
+ """
135
+ if not command or not str(command).strip():
136
+ return False
137
+ if any(op in command for op in ("&&", "||", ";", "|", "(", ")")):
138
+ return True
139
+ try:
140
+ tokens = shlex.split(command, posix=True)
141
+ except ValueError:
142
+ return False
143
+ while tokens and _ENV_ASSIGN_RE.match(tokens[0]):
144
+ tokens.pop(0)
145
+ if not tokens:
146
+ return False
147
+ binary = tokens[0]
148
+ if "/" in binary:
149
+ return Path(binary).expanduser().exists()
150
+ return shutil.which(binary) is not None
151
+
152
+
153
+ def _collect_test_manifests(
154
+ project_root: Path, *, max_files: int = 24, max_bytes: int = 4000
155
+ ) -> list[dict]:
156
+ """Gather manifest snippets (repo root + immediate subdirs) for discovery."""
157
+ root = Path(project_root)
158
+ docs: list[dict] = []
159
+ dirs: list[Path] = [root]
160
+ try:
161
+ for child in sorted(root.iterdir(), key=lambda p: p.name):
162
+ if child.is_dir() and child.name not in _SKIP_DIRS and not child.name.startswith("."):
163
+ dirs.append(child)
164
+ except OSError:
165
+ return docs
166
+
167
+ for directory in dirs:
168
+ for name in _MANIFEST_NAMES:
169
+ path = directory / name
170
+ if not path.is_file():
171
+ continue
172
+ try:
173
+ content = path.read_text(encoding="utf-8", errors="replace")[:max_bytes]
174
+ except OSError:
175
+ continue
176
+ rel = path.relative_to(root)
177
+ docs.append({"path": str(rel), "content": content})
178
+ if len(docs) >= max_files:
179
+ return docs
180
+ tests_dir = directory / "tests"
181
+ if tests_dir.is_dir():
182
+ try:
183
+ names = sorted(p.name for p in tests_dir.iterdir() if p.is_file())[:40]
184
+ except OSError:
185
+ names = []
186
+ rel = tests_dir.relative_to(root)
187
+ docs.append({"path": f"{rel}/ (listing)", "content": "\n".join(names)})
188
+ if len(docs) >= max_files:
189
+ return docs
190
+ return docs
191
+
192
+
193
+ def _discover_test_command_via_cloud(
194
+ *,
195
+ project_root: Path,
196
+ constitution,
197
+ execution_mode: str,
198
+ auth_ctx,
199
+ model_hint: str | None,
200
+ apply_byok_payload: Callable[[dict, object], None] | None,
201
+ ) -> str:
202
+ """Ask the server to infer a test command from the project's manifests.
203
+
204
+ Best-effort: any transport/parse/auth issue returns ``""`` so the caller
205
+ falls back to the existing defer-when-unverifiable behavior.
206
+ """
207
+ documents = _collect_test_manifests(project_root)
208
+ if not documents:
209
+ return ""
210
+ payload = {
211
+ "stage": "rr_discover_testcmd",
212
+ "constitution_hash": constitution.content_hash,
213
+ "constitution_body": getattr(constitution, "body", "") or "",
214
+ "model_hint": model_hint,
215
+ "documents": documents,
216
+ "execution_mode": execution_mode,
217
+ }
218
+ if execution_mode == "cloud_byok" and apply_byok_payload is not None:
219
+ apply_byok_payload(payload, constitution)
220
+ idem = hashlib.sha256(
221
+ "|".join(
222
+ [auth_ctx.account_id, execution_mode, constitution.content_hash, "rr_discover_testcmd"]
223
+ ).encode("utf-8")
224
+ ).hexdigest()
225
+ try:
226
+ resp = httpx.post(
227
+ f"{platform_url()}/v1/inference/refactor",
228
+ headers={
229
+ "Authorization": f"Bearer {auth_ctx.key.key}",
230
+ "Content-Type": "application/json",
231
+ "Idempotency-Key": idem,
232
+ },
233
+ json=payload,
234
+ timeout=45.0,
235
+ )
236
+ except httpx.HTTPError:
237
+ return ""
238
+ if resp.status_code != 200:
239
+ return ""
240
+ try:
241
+ data = resp.json() if resp.content else {}
242
+ except ValueError:
243
+ return ""
244
+ return str((data or {}).get("test_command", "") or "").strip()
245
+
246
+
247
+ def _resolve_cloud_test_command(
248
+ *,
249
+ project_root: Path,
250
+ config: dict,
251
+ constitution,
252
+ execution_mode: str,
253
+ auth_ctx,
254
+ model_hint: str | None,
255
+ apply_byok_payload: Callable[[dict, object], None] | None,
256
+ ) -> tuple[str | None, str]:
257
+ """Resolve a test command for a cloud run; return ``(command, note)``.
258
+
259
+ Layered resolution so the developer usually needs no config:
260
+ 1. configured ``verification.tests.command`` or root-marker detection,
261
+ 2. deterministic monorepo discovery (subproject test setups),
262
+ 3. agentic discovery (server infers from manifests), preflight-validated.
263
+ """
264
+ configured = resolve_test_command(project_root, config)
265
+ if configured:
266
+ return configured, ""
267
+
268
+ discovered = discover_test_command(project_root)
269
+ if discovered:
270
+ return discovered, f"auto-detected test command: {discovered}"
271
+
272
+ suggested = _discover_test_command_via_cloud(
273
+ project_root=project_root,
274
+ constitution=constitution,
275
+ execution_mode=execution_mode,
276
+ auth_ctx=auth_ctx,
277
+ model_hint=model_hint,
278
+ apply_byok_payload=apply_byok_payload,
279
+ )
280
+ if suggested and _client_preflight_command(project_root, suggested):
281
+ return suggested, f"server-suggested test command: {suggested}"
282
+ if suggested:
283
+ return None, (
284
+ f"server suggested test command '{suggested}' but its toolchain was not "
285
+ "found on PATH; skipped."
286
+ )
287
+ return None, ""
288
+
289
+
290
+ class _Session:
291
+ """Transport for the RR session endpoints with status-code error mapping."""
292
+
293
+ def __init__(self, *, auth_ctx, execution_mode: str, constitution_hash: str) -> None:
294
+ self._auth = auth_ctx
295
+ self._mode = execution_mode
296
+ self._consti = constitution_hash
297
+ self._base = f"{platform_url()}/v1/rr"
298
+
299
+ def _idempotency_key(self, path: str, payload: dict) -> str:
300
+ """Deterministic key per (account, mode, consti, rr, step, attempt).
301
+
302
+ Makes a step POST safe to retry without duplicating work or double
303
+ charging (FR6). Distinct RRs/attempts/steps get distinct keys so unrelated
304
+ attempts never collapse into one replayed result.
305
+ """
306
+ seed = "|".join(
307
+ [
308
+ self._auth.account_id,
309
+ self._mode,
310
+ self._consti,
311
+ path,
312
+ str(payload.get("rr_id", "")),
313
+ str(payload.get("step", "")),
314
+ str(payload.get("attempt", "")),
315
+ ]
316
+ )
317
+ return hashlib.sha256(seed.encode("utf-8")).hexdigest()
318
+
319
+ def _post(self, path: str, payload: dict) -> dict:
320
+ try:
321
+ resp = httpx.post(
322
+ f"{self._base}{path}",
323
+ headers={
324
+ "Authorization": f"Bearer {self._auth.key.key}",
325
+ "Content-Type": "application/json",
326
+ "Idempotency-Key": self._idempotency_key(path, payload),
327
+ },
328
+ json=payload,
329
+ timeout=45.0,
330
+ )
331
+ except httpx.HTTPError as exc:
332
+ raise CloudRRError(f"Could not reach cloud RR service: {exc}") from exc
333
+
334
+ if resp.status_code == 401:
335
+ raise CloudRRError("Cloud RR unauthorized (developer key invalid or revoked).")
336
+ if resp.status_code == 402:
337
+ if self._mode == "cloud_byok":
338
+ raise CloudRRError(
339
+ "Cloud BYOK is not enabled for this account (missing `run:byok`). "
340
+ "Contact your admin to enable BYOK cloud execution."
341
+ )
342
+ raise CloudRRError("Cloud RR requires paid tier (`mode: cloud_managed`).")
343
+ if resp.status_code == 404 and self._mode == "cloud_byok":
344
+ raise CloudRRError(
345
+ "BYOK credential not found or not owned by this account. "
346
+ "Check `refactor cloud credentials list` and your `credential_ref`."
347
+ )
348
+ if resp.status_code in (400, 403, 409):
349
+ detail = ""
350
+ try:
351
+ detail = str((resp.json() or {}).get("detail", ""))
352
+ except Exception:
353
+ detail = resp.text[:300]
354
+ raise CloudRRError(detail or f"Cloud RR error ({resp.status_code}).")
355
+ if resp.status_code == 429:
356
+ raise CloudRRError("Cloud RR quota exhausted or rate-limited.")
357
+ if resp.status_code >= 400:
358
+ raise CloudRRError(f"Cloud RR failed ({resp.status_code}): {resp.text[:300]}")
359
+ return resp.json() if resp.content else {}
360
+
361
+ def open(self, payload: dict) -> dict:
362
+ return self._post("/session", payload)
363
+
364
+ def step(self, payload: dict) -> dict:
365
+ return self._post("/session/step", payload)
366
+
367
+
368
+ def run_cloud_refactor_requests(
369
+ *,
370
+ project_root: Path,
371
+ constitution,
372
+ config: dict,
373
+ requests=None,
374
+ model_hint: str | None = None,
375
+ progress_cb: Callable[[dict], None] | None = None,
376
+ approval_cb: Callable[[dict], bool] | None = None,
377
+ apply_byok_payload: Callable[[dict, object], None] | None = None,
378
+ ) -> RunArtifact:
379
+ """Drive pending RRs through the cloud server-run loop and return a summary.
380
+
381
+ ``progress_cb`` / ``approval_cb`` are the SAME callbacks local uses
382
+ (``_rr_progress_logger`` / ``_rr_approval_prompt``), so the run streams the
383
+ same events and prompts per RR mid-run (FR8). ``apply_byok_payload`` is the
384
+ shared BYOK resolver injected by the caller to avoid a circular import.
385
+ """
386
+ project_root = Path(project_root)
387
+ progress_cb = progress_cb or (lambda e: None)
388
+ approval_cb = approval_cb or (lambda r: False)
389
+
390
+ execution_mode = str(constitution.get_setting("mode", "cloud_managed") or "cloud_managed")
391
+ auth_ctx = ensure_authenticated(project_root)
392
+ test_command, testcmd_note = _resolve_cloud_test_command(
393
+ project_root=project_root,
394
+ config=config,
395
+ constitution=constitution,
396
+ execution_mode=execution_mode,
397
+ auth_ctx=auth_ctx,
398
+ model_hint=model_hint,
399
+ apply_byok_payload=apply_byok_payload,
400
+ )
401
+
402
+ if requests is None:
403
+ requests = pending_requests(project_root)
404
+
405
+ run_store = create_run(project_root)
406
+ run_id = run_store.run_id
407
+ resolved_model = model_hint or str(
408
+ constitution.get_setting("model_id", "managed-default") or "managed-default"
409
+ )
410
+
411
+ _emit(progress_cb, phase="rr", event="run_start", total_requests=len(requests),
412
+ test_command=test_command or "", execution_env="cloud")
413
+
414
+ outcomes: list[dict] = []
415
+ counts = {"applied": 0, "denied": 0, "failed": 0, "deferred": 0}
416
+ notes: list[str] = []
417
+ if testcmd_note:
418
+ notes.append(testcmd_note)
419
+ stopped = False
420
+ stop_reason = ""
421
+ model_id = resolved_model
422
+
423
+ if not requests:
424
+ return _build_artifact(
425
+ run_store, run_id, constitution, model_id, outcomes, counts,
426
+ notes, stopped=False, stop_reason="", test_command=test_command,
427
+ )
428
+
429
+ session = _Session(
430
+ auth_ctx=auth_ctx,
431
+ execution_mode=execution_mode,
432
+ constitution_hash=constitution.content_hash,
433
+ )
434
+
435
+ # --- open the session ---------------------------------------------------
436
+ open_payload = {
437
+ "execution_mode": execution_mode,
438
+ "constitution_hash": constitution.content_hash,
439
+ "constitution_body": getattr(constitution, "body", "") or "",
440
+ "model_hint": resolved_model,
441
+ "rrs": [
442
+ {
443
+ "rr_id": rr.rr_id,
444
+ "rr_context": {
445
+ "location": rr.location,
446
+ "file_path": rr.file_path,
447
+ "category": rr.category,
448
+ "severity": rr.severity,
449
+ "recommendation": rr.recommendation,
450
+ "operation_type": rr.operation_type,
451
+ },
452
+ }
453
+ for rr in requests
454
+ ],
455
+ }
456
+ if execution_mode == "cloud_byok" and apply_byok_payload is not None:
457
+ apply_byok_payload(open_payload, constitution)
458
+
459
+ action = session.open(open_payload)
460
+ token = action.get("session_token", "")
461
+
462
+ # Map rr_id -> RefactorRequest for status updates.
463
+ by_id = {rr.rr_id: rr for rr in requests}
464
+ attempts_by_id: dict[str, int] = {}
465
+ started: set[str] = set()
466
+
467
+ def _start_rr(rr_id: str):
468
+ """Emit rr_start (once) and send the `start` step for ``rr_id``."""
469
+ rr = by_id.get(rr_id)
470
+ if rr is not None and rr_id not in started:
471
+ started.add(rr_id)
472
+ rr.status = IN_PROGRESS
473
+ rr.resolved_in_run = run_id
474
+ save_request(project_root, rr)
475
+ _emit(progress_cb, phase="rr", event="rr_start", rr_id=rr_id, location=rr.location)
476
+ return session.step(_step_payload(token, rr_id, "start", by_id, project_root))
477
+
478
+ # --- drive the loop -----------------------------------------------------
479
+ # Uniform contract: the server returns the next action; the client executes a
480
+ # primitive and posts the result. Terminal per-RR verdicts carry `next_rr_id`
481
+ # (empty => finished). We always send the current file source so the server
482
+ # can (re)build prompts. A generous iteration cap guards against runaway.
483
+ current_rr_id = action.get("rr_id", "")
484
+ if str(action.get("action", "")) == "next_rr" and current_rr_id:
485
+ action = _start_rr(current_rr_id)
486
+ token = action.get("session_token", token)
487
+
488
+ guard = 0
489
+ max_iters = max(50, len(requests) * 25)
490
+ while guard < max_iters:
491
+ guard += 1
492
+ act = str(action.get("action", ""))
493
+ model_id = action.get("model_id", model_id) or model_id
494
+
495
+ if act == "done":
496
+ break
497
+
498
+ if act == "run_baseline_test":
499
+ current_rr_id = action.get("rr_id", "") or current_rr_id
500
+ _emit(progress_cb, phase="rr", event="test_generating", rr_id=current_rr_id)
501
+ _write_generated_test(project_root, current_rr_id, action, by_id)
502
+ passed, detail = _run_test(project_root, test_command)
503
+ _emit(progress_cb, phase="rr", event="baseline_test", rr_id=current_rr_id,
504
+ passed=passed, detail=detail, execution_env="host")
505
+ action = session.step(
506
+ _step_payload(token, current_rr_id, "baseline_result", by_id, project_root,
507
+ baseline_passed=passed, baseline_detail=detail)
508
+ )
509
+ token = action.get("session_token", token)
510
+ continue
511
+
512
+ if act == "show_proposal":
513
+ current_rr_id = action.get("rr_id", "") or current_rr_id
514
+ rr = by_id.get(current_rr_id)
515
+ diff_text = str(action.get("diff_text", "") or "")
516
+ attempt = int(action.get("attempt", 1) or 1)
517
+ attempts_by_id[current_rr_id] = attempt
518
+ ok, why = validate_diff(project_root, diff_text)
519
+ if not ok:
520
+ _emit(progress_cb, phase="rr", event="proposal_invalid", attempt=attempt, reason=why)
521
+ action = session.step(
522
+ _step_payload(token, current_rr_id, "verify_result", by_id, project_root,
523
+ attempt=attempt,
524
+ verify_passed=False, verify_detail=f"invalid diff: {why}")
525
+ )
526
+ token = action.get("session_token", token)
527
+ continue
528
+ _emit(progress_cb, phase="rr", event="approval_requested", rr_id=current_rr_id)
529
+ approved = bool(approval_cb({
530
+ "rr_id": current_rr_id,
531
+ "location": getattr(rr, "location", current_rr_id),
532
+ "attempt": attempt,
533
+ "summary": action.get("summary", ""),
534
+ "risk": action.get("risk", "medium"),
535
+ "diff_text": diff_text,
536
+ }))
537
+ _emit(progress_cb, phase="rr", event="approval_decision", rr_id=current_rr_id, approved=approved)
538
+ _PENDING_DIFF[current_rr_id] = diff_text
539
+ action = session.step(
540
+ _step_payload(token, current_rr_id, "approval", by_id, project_root,
541
+ attempt=attempt, approved=approved)
542
+ )
543
+ token = action.get("session_token", token)
544
+ continue
545
+
546
+ if act in ("apply_result", "deferred"):
547
+ verdict = str(action.get("verdict", "") or "")
548
+ if verdict == "":
549
+ # Server approved: client applies + verifies, then posts result.
550
+ current_rr_id = action.get("rr_id", "") or current_rr_id
551
+ diff_text = _PENDING_DIFF.get(current_rr_id, "")
552
+ passed, detail, files = _apply_and_verify(
553
+ project_root, run_store.run_dir, diff_text, test_command
554
+ )
555
+ if passed:
556
+ _emit(progress_cb, phase="rr", event="rr_applied", rr_id=current_rr_id, files=files)
557
+ else:
558
+ _emit(progress_cb, phase="rr", event="rr_reverted", rr_id=current_rr_id,
559
+ attempt=attempts_by_id.get(current_rr_id, 1), detail=detail)
560
+ action = session.step(
561
+ _step_payload(token, current_rr_id, "verify_result", by_id, project_root,
562
+ attempt=attempts_by_id.get(current_rr_id, 1),
563
+ verify_passed=passed, verify_detail=detail)
564
+ )
565
+ token = action.get("session_token", token)
566
+ continue
567
+ # Terminal verdict for the completed RR.
568
+ completed_id = action.get("rr_id", "") or current_rr_id
569
+ _record_outcome(
570
+ project_root, by_id, completed_id, verdict, action.get("message", ""),
571
+ attempts_by_id.get(completed_id, 0 if verdict == "deferred" else 1),
572
+ outcomes, counts, progress_cb,
573
+ )
574
+ if verdict == "failed":
575
+ stopped = True
576
+ stop_reason = str(action.get("message", "") or "refactor request failed")
577
+ _emit(progress_cb, phase="rr", event="run_stopped", rr_id=completed_id, reason=stop_reason)
578
+ break
579
+ next_id = str(action.get("next_rr_id", "") or "")
580
+ if not next_id:
581
+ break
582
+ current_rr_id = next_id
583
+ action = _start_rr(next_id)
584
+ token = action.get("session_token", token)
585
+ continue
586
+
587
+ notes.append(f"unexpected cloud action '{act}'")
588
+ break
589
+
590
+ return _build_artifact(
591
+ run_store, run_id, constitution, model_id, outcomes, counts,
592
+ notes, stopped=stopped, stop_reason=stop_reason, test_command=test_command,
593
+ )
594
+
595
+
596
+ # Diffs approved but not yet applied, keyed by rr_id (client-local, per run).
597
+ _PENDING_DIFF: dict[str, str] = {}
598
+
599
+
600
+ def _step_payload(token, rr_id, step, by_id, project_root, *, attempt: int = 0, **kw) -> dict:
601
+ rr = by_id.get(rr_id) if rr_id else None
602
+ source = _read_source(project_root, rr.file_path) if rr is not None else ""
603
+ payload = {
604
+ "session_token": token,
605
+ "rr_id": rr_id,
606
+ "step": step,
607
+ "source": source,
608
+ "attempt": int(attempt),
609
+ }
610
+ payload.update(kw)
611
+ return payload
612
+
613
+
614
+ def _write_generated_test(project_root: Path, rr_id: str, action: dict, by_id: dict) -> None:
615
+ content = str(action.get("test_content", "") or "")
616
+ if not content:
617
+ return
618
+ test_path = str(action.get("test_path", "") or "")
619
+ name = Path(test_path).name if test_path else f"{rr_id}.test.txt"
620
+ test_dir = rr_test_dir(project_root, rr_id)
621
+ (test_dir / name).write_text(content, encoding="utf-8")
622
+ rr = by_id.get(rr_id)
623
+ if rr is not None:
624
+ rr.rrtest_path = str(test_dir / name)
625
+ save_request(project_root, rr)
626
+
627
+
628
+ def _apply_and_verify(
629
+ project_root: Path, run_dir: Path, diff_text: str, test_command
630
+ ) -> tuple[bool, str, list[str]]:
631
+ """Apply the diff (with backup), run the test; revert on red.
632
+
633
+ Returns ``(passed, detail, changed_files)``. ``changed_files`` is computed
634
+ from the diff BEFORE applying (the tree is mutated in place afterward, so it
635
+ cannot be recomputed against the modified files).
636
+ """
637
+ if not diff_text:
638
+ return False, "empty diff", []
639
+ try:
640
+ changes = compute_file_changes(project_root, [diff_text])
641
+ except Exception as exc:
642
+ return False, f"could not compute changes: {exc}", []
643
+ if not changes:
644
+ return False, "diff produced no file changes", []
645
+ changed_files = list(changes.keys())
646
+ try:
647
+ apply_changes(project_root, run_dir, changes)
648
+ except Exception as exc:
649
+ return False, f"apply failed: {exc}", changed_files
650
+ passed, detail = _run_test(project_root, test_command)
651
+ if not passed:
652
+ try:
653
+ revert_from_backup(project_root, run_dir)
654
+ except Exception as exc: # keep going; report both
655
+ detail = f"{detail}; revert error: {exc}"
656
+ return passed, detail, changed_files
657
+
658
+
659
+ def _record_outcome(project_root, by_id, rr_id, verdict, message, attempts,
660
+ outcomes: list, counts: dict, progress_cb) -> None:
661
+ status_map = {
662
+ "applied": APPLIED,
663
+ "denied": DENIED,
664
+ "failed": FAILED,
665
+ "deferred": DEFERRED,
666
+ }
667
+ rr = by_id.get(rr_id)
668
+ if rr is not None:
669
+ rr.status = status_map.get(verdict, rr.status)
670
+ rr.attempt_count = max(int(getattr(rr, "attempt_count", 0) or 0), int(attempts or 0))
671
+ save_request(project_root, rr)
672
+ outcomes.append({
673
+ "location": getattr(rr, "location", rr_id) if rr is not None else rr_id,
674
+ "operation_type": getattr(rr, "operation_type", "content_patch") if rr is not None else "content_patch",
675
+ "outcome": verdict,
676
+ "attempts": int(attempts or 0),
677
+ })
678
+ if verdict in counts:
679
+ counts[verdict] += 1
680
+ ev = {
681
+ "applied": "rr_applied",
682
+ "denied": None,
683
+ "failed": "rr_failed",
684
+ "deferred": "rr_deferred",
685
+ }.get(verdict)
686
+ if ev == "rr_deferred":
687
+ _emit(progress_cb, phase="rr", event="rr_deferred", rr_id=rr_id, reason=message)
688
+ elif ev == "rr_failed":
689
+ _emit(progress_cb, phase="rr", event="rr_failed", rr_id=rr_id, reason=message)
690
+
691
+
692
+ def _build_artifact(run_store, run_id, constitution, model_id, outcomes, counts,
693
+ notes, *, stopped, stop_reason, test_command) -> RunArtifact:
694
+ gate = GateResult(status="blocked" if stopped else "passed")
695
+ applied = counts["applied"]
696
+ summary = (
697
+ f"Cloud refactor requests: applied={applied} denied={counts['denied']} "
698
+ f"failed={counts['failed']} deferred={counts['deferred']}"
699
+ )
700
+ artifact = RunArtifact(
701
+ run_id=run_id,
702
+ findings=[],
703
+ patches=[],
704
+ generated_tests=[],
705
+ gate=gate,
706
+ summary=summary,
707
+ constitution_hash=constitution.content_hash,
708
+ model_id=model_id,
709
+ notes=notes,
710
+ execution_mode="rr_cloud",
711
+ context_stats={"mode": "rr_cloud", "test_command": test_command or ""},
712
+ agent_trace_summary={
713
+ "outcomes": outcomes,
714
+ "applied": counts["applied"],
715
+ "denied": counts["denied"],
716
+ "failed": counts["failed"],
717
+ "deferred": counts["deferred"],
718
+ "stopped": stopped,
719
+ "stop_reason": stop_reason,
720
+ },
721
+ )
722
+ run_store.write_artifact(artifact, target=".", status="completed")
723
+ return artifact
724
+
725
+
726
+ def _emit(progress_cb, **event) -> None:
727
+ try:
728
+ progress_cb(event)
729
+ except Exception:
730
+ pass
@@ -772,155 +772,6 @@ def _apply_byok_payload(payload: dict, constitution) -> None:
772
772
  )
773
773
 
774
774
 
775
- def _post_cloud_inference(*, payload: dict, idempotency_key: str, auth_ctx, execution_mode: str) -> dict:
776
- """POST a cloud inference request and return the parsed JSON body.
777
-
778
- Centralizes transport + status-code error mapping so the per-RR proposal
779
- path surfaces the same remediation messages as review/refactor.
780
- """
781
- try:
782
- response = httpx.post(
783
- f"{platform_url()}/v1/inference/refactor",
784
- headers={
785
- "Authorization": f"Bearer {auth_ctx.key.key}",
786
- "Content-Type": "application/json",
787
- "Idempotency-Key": idempotency_key,
788
- },
789
- json=payload,
790
- timeout=45.0,
791
- )
792
- except httpx.HTTPError as exc:
793
- raise RuntimeError(f"Could not reach cloud inference service: {exc}") from exc
794
-
795
- if response.status_code == 401:
796
- raise RuntimeError("Cloud inference unauthorized (developer key invalid or revoked).")
797
- if response.status_code == 402:
798
- if execution_mode == "cloud_byok":
799
- raise RuntimeError(
800
- "Cloud BYOK is not enabled for this account (missing `run:byok`). "
801
- "Contact your admin to enable BYOK cloud execution."
802
- )
803
- raise RuntimeError("Cloud inference requires paid tier (`mode: cloud_managed`).")
804
- if response.status_code == 404 and execution_mode == "cloud_byok":
805
- raise RuntimeError(
806
- "BYOK credential not found or not owned by this account. "
807
- "Check `refactor cloud credentials list` and your `credential_ref`."
808
- )
809
- if response.status_code == 429:
810
- raise RuntimeError("Cloud inference quota exhausted or rate-limited.")
811
- if response.status_code >= 400:
812
- raise RuntimeError(f"Cloud inference failed ({response.status_code}): {response.text[:500]}")
813
-
814
- return response.json() if response.content else {}
815
-
816
-
817
- class _CloudRRProvider:
818
- """Lightweight provider stub for cloud RR execution (R22).
819
-
820
- The executor reads ``provider.model_id`` for the artifact and would call
821
- ``provider.complete(...)`` for proposals/tests. In cloud mode the proposal
822
- and test-gen hooks are supplied, so ``complete`` is never invoked; it raises
823
- to make any accidental local-prompt path explicit rather than silent.
824
- """
825
-
826
- def __init__(self, *, model_id: str) -> None:
827
- self.model_id = model_id
828
- self.provider_name = "cloud"
829
- self.supports_refactor = True
830
-
831
- def complete(self, system: str, user: str) -> str: # pragma: no cover - guard
832
- raise RuntimeError(
833
- "cloud RR execution must use server-side proposal hooks; "
834
- "provider.complete should not be called."
835
- )
836
-
837
-
838
- def _cloud_rr_hooks(
839
- *,
840
- project_root: Path,
841
- constitution,
842
- model_hint: str | None,
843
- ) -> tuple[Callable, Callable, str]:
844
- """Build (propose_fn, gen_test_fn, model_id) for cloud per-RR execution (R22).
845
-
846
- The returned callables match the executor's hook contract. They send the
847
- single RR's context + current source to the platform, which builds the
848
- proprietary prompts server-side and returns a proposal / characterization
849
- test. Idempotency keys include the RR id and attempt so retries of distinct
850
- RRs/attempts never collapse into a replayed result.
851
- """
852
- execution_mode = _cloud_execution_mode(constitution) or "cloud_managed"
853
- auth_ctx = ensure_authenticated(project_root)
854
- constitution_body = getattr(constitution, "body", "") or ""
855
- resolved_model = model_hint or str(
856
- constitution.get_setting("model_id", "managed-default") or "managed-default"
857
- )
858
-
859
- def _rr_context(rr) -> dict:
860
- return {
861
- "location": getattr(rr, "location", ""),
862
- "file_path": getattr(rr, "file_path", ""),
863
- "category": getattr(rr, "category", ""),
864
- "severity": getattr(rr, "severity", ""),
865
- "recommendation": getattr(rr, "recommendation", ""),
866
- }
867
-
868
- def _base_payload(stage: str, rr, source: str) -> dict:
869
- payload = {
870
- "stage": stage,
871
- "execution_mode": execution_mode,
872
- "constitution_hash": constitution.content_hash,
873
- "constitution_body": constitution_body,
874
- "rr_context": _rr_context(rr),
875
- "source": source,
876
- "max_alerts": int(constitution.get_setting("max_alerts_per_run", 5) or 5),
877
- "model_hint": resolved_model,
878
- }
879
- if execution_mode == "cloud_byok":
880
- _apply_byok_payload(payload, constitution)
881
- return payload
882
-
883
- def _idempotency(stage: str, rr, attempt: int) -> str:
884
- seed = "|".join(
885
- [
886
- auth_ctx.account_id,
887
- execution_mode,
888
- stage,
889
- constitution.content_hash,
890
- str(getattr(rr, "rr_id", "")),
891
- str(int(attempt)),
892
- ]
893
- )
894
- return hashlib.sha256(seed.encode("utf-8")).hexdigest()
895
-
896
- def propose_fn(rr, source: str, prior_failures: list[str]) -> dict:
897
- # Attempt number is derived from prior failures so each retry gets a
898
- # distinct idempotency key (1-based).
899
- attempt = len(prior_failures or []) + 1
900
- payload = _base_payload("rr_propose", rr, source)
901
- payload["prior_failures"] = list(prior_failures or [])
902
- data = _post_cloud_inference(
903
- payload=payload,
904
- idempotency_key=_idempotency("rr_propose", rr, attempt),
905
- auth_ctx=auth_ctx,
906
- execution_mode=execution_mode,
907
- )
908
- proposal = data.get("proposal") or {}
909
- if not isinstance(proposal, dict):
910
- return {}
911
- return proposal
912
-
913
- def gen_test_fn(rr, source: str) -> dict:
914
- # Server-side characterization-test generation is not part of Phase 1.
915
- # Returning no test makes the executor fall back to the configured test
916
- # command for behavioral verification (parity with a local run that has
917
- # no generated test). This keeps cloud RR execution safe today and lets
918
- # a future phase add a dedicated server test-gen stage.
919
- return {}
920
-
921
- return propose_fn, gen_test_fn, resolved_model
922
-
923
-
924
775
  def _load_project() -> tuple[Path, object, object]:
925
776
  files = find_project_files()
926
777
  if not files:
@@ -2105,7 +1956,7 @@ def code(
2105
1956
  help="Skip interactive issue approvals in agentic code mode.",
2106
1957
  ),
2107
1958
  on_rr_failure: str = typer.Option(
2108
- "stop",
1959
+ "continue",
2109
1960
  "--on-rr-failure",
2110
1961
  help="RR failure behavior: stop | continue | defer.",
2111
1962
  ),
@@ -2292,43 +2143,49 @@ def code(
2292
2143
  console.print("[green]No issues to refactor.[/green]")
2293
2144
  return
2294
2145
 
2295
- from refactor_core.execution.rr_executor import run_refactor_requests
2146
+ rr_config = (
2147
+ dict(getattr(project_config, "settings", {}) or {})
2148
+ | ({"sandbox": {"enabled": True}} if sandbox_on else {})
2149
+ | ({"_sandbox_context": sandbox_ctx} if sandbox_ctx else {})
2150
+ ) or dict(getattr(constitution, "settings", {}) or {})
2296
2151
 
2297
- # Cloud and local both drive the SAME per-RR interactive loop (R22 cloud
2298
- # RR parity). The only difference is where proposals come from: cloud
2299
- # passes server-side proposal hooks; local uses the local provider and
2300
- # client-side prompt builders. This guarantees identical UX (proposal
2301
- # diff + approval prompt), status transitions, and outcomes table.
2302
- cloud_propose_fn = None
2303
- cloud_gen_test_fn = None
2304
- rr_provider = selected_provider
2305
2152
  if cloud:
2306
- cloud_propose_fn, cloud_gen_test_fn, cloud_model_id = _cloud_rr_hooks(
2307
- project_root=project_root,
2308
- constitution=constitution,
2309
- model_hint=provider,
2310
- )
2311
- rr_provider = _CloudRRProvider(model_id=cloud_model_id)
2312
-
2313
- with console.status("[cyan]code[/cyan] running...", spinner="dots") as status:
2314
- rr_config = (
2315
- dict(getattr(project_config, "settings", {}) or {})
2316
- | ({"sandbox": {"enabled": True}} if sandbox_on else {})
2317
- | ({"_sandbox_context": sandbox_ctx} if sandbox_ctx else {})
2318
- ) or dict(getattr(constitution, "settings", {}) or {})
2319
- artifact = run_refactor_requests(
2320
- project_root=project_root,
2321
- constitution=constitution,
2322
- provider=rr_provider,
2323
- requests=requests,
2324
- config=rr_config,
2325
- progress_cb=_rr_progress_logger(status=status),
2326
- approval_cb=_rr_approval_prompt(auto_approve=auto_approve),
2327
- on_rr_failure=on_rr_failure,
2328
- equivalence_mode=equivalence,
2329
- propose_fn=cloud_propose_fn,
2330
- gen_test_fn=cloud_gen_test_fn,
2331
- )
2153
+ # Cloud modes: the proprietary per-RR loop runs SERVER-SIDE (R23; doc
2154
+ # 35). The client is a thin executor speaking the RR session protocol
2155
+ # and uses ONLY client-safe modules — it never imports the engine-only
2156
+ # ``rr_executor``. It reuses the SAME progress logger + approval prompt
2157
+ # as local so the run streams events and asks for approval per RR,
2158
+ # mid-run (identical UX), with tests guarding every change.
2159
+ from refactorai_cli.cloud_rr import run_cloud_refactor_requests
2160
+
2161
+ with console.status("[cyan]code[/cyan] running (cloud)...", spinner="dots") as status:
2162
+ artifact = run_cloud_refactor_requests(
2163
+ project_root=project_root,
2164
+ constitution=constitution,
2165
+ config=rr_config,
2166
+ requests=requests,
2167
+ model_hint=provider,
2168
+ progress_cb=_rr_progress_logger(status=status),
2169
+ approval_cb=_rr_approval_prompt(auto_approve=auto_approve),
2170
+ apply_byok_payload=_apply_byok_payload,
2171
+ )
2172
+ else:
2173
+ # Local mode is unchanged: dispatch to the local executor with the
2174
+ # local provider and no cloud hooks.
2175
+ from refactor_core.execution.rr_executor import run_refactor_requests
2176
+
2177
+ with console.status("[cyan]code[/cyan] running...", spinner="dots") as status:
2178
+ artifact = run_refactor_requests(
2179
+ project_root=project_root,
2180
+ constitution=constitution,
2181
+ provider=selected_provider,
2182
+ requests=requests,
2183
+ config=rr_config,
2184
+ progress_cb=_rr_progress_logger(status=status),
2185
+ approval_cb=_rr_approval_prompt(auto_approve=auto_approve),
2186
+ on_rr_failure=on_rr_failure,
2187
+ equivalence_mode=equivalence,
2188
+ )
2332
2189
  except typer.Exit:
2333
2190
  # Intentional exits (e.g. gate-blocked apply) keep their own code/message.
2334
2191
  raise
@@ -2370,7 +2227,7 @@ def requests(
2370
2227
  help="Auto-approve retry-deferred proposals.",
2371
2228
  ),
2372
2229
  on_rr_failure: str = typer.Option(
2373
- "stop",
2230
+ "continue",
2374
2231
  "--on-rr-failure",
2375
2232
  help="RR failure behavior for retry: stop | continue | defer.",
2376
2233
  ),
@@ -3627,7 +3484,7 @@ def _report_structural_readiness(project_root: Path, config) -> None:
3627
3484
  if not isinstance(structural, dict):
3628
3485
  structural = {}
3629
3486
  eq_mode = str(structural.get("equivalence_mode", "strict") or "strict")
3630
- rr_policy = str(structural.get("on_rr_failure", "stop") or "stop")
3487
+ rr_policy = str(structural.get("on_rr_failure", "continue") or "continue")
3631
3488
  rewrite_mode = str(structural.get("reference_rewrite", "auto") or "auto")
3632
3489
  langs = structural.get("languages", [])
3633
3490
  rollout = str(structural.get("rollout", "default") or "default")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.3.2
3
+ Version: 0.3.4
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.0.2
11
+ Requires-Dist: refactorai-core>=3.0.4
12
12
 
13
13
  # refactorai-cli
14
14
 
@@ -54,7 +54,7 @@ python -m twine upload ./refactorai-cli/dist/*
54
54
  ## Install test (local)
55
55
 
56
56
  ```bash
57
- python -m pip install ./refactorai-cli/dist/refactorai_cli-0.3.2-py3-none-any.whl
57
+ python -m pip install ./refactorai-cli/dist/refactorai_cli-0.3.4-py3-none-any.whl
58
58
  refactor --version
59
59
  ```
60
60
 
@@ -3,6 +3,7 @@ pyproject.toml
3
3
  refactorai_cli/__init__.py
4
4
  refactorai_cli/auth.py
5
5
  refactorai_cli/client.py
6
+ refactorai_cli/cloud_rr.py
6
7
  refactorai_cli/control_plane.py
7
8
  refactorai_cli/credentials.py
8
9
  refactorai_cli/local_constitution.py
@@ -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.0.2
5
+ refactorai-core>=3.0.4
File without changes