refactorai-cli 0.2.26__tar.gz → 0.2.28__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 (35) hide show
  1. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/PKG-INFO +35 -2
  2. refactorai_cli-0.2.28/README.md +80 -0
  3. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/pyproject.toml +1 -1
  4. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/__init__.py +1 -1
  5. refactorai_cli-0.2.28/refactorai_cli/commands/cloud_cmds.py +176 -0
  6. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/commands/run_cmds.py +231 -7
  7. refactorai_cli-0.2.28/refactorai_cli/commands/runtime_proxy_cmds.py +224 -0
  8. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/commands/setup_cmds.py +15 -0
  9. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/main.py +12 -1
  10. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/setup_flow.py +90 -9
  11. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli.egg-info/PKG-INFO +35 -2
  12. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli.egg-info/SOURCES.txt +1 -0
  13. refactorai_cli-0.2.26/README.md +0 -47
  14. refactorai_cli-0.2.26/refactorai_cli/commands/runtime_proxy_cmds.py +0 -143
  15. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/auth.py +0 -0
  16. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/client.py +0 -0
  17. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/commands/__init__.py +0 -0
  18. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/commands/auth_cmds.py +0 -0
  19. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/commands/engine_cmds.py +0 -0
  20. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/commands/model_cmds.py +0 -0
  21. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/commands/rules_cmds.py +0 -0
  22. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/commands/runtime_cmds.py +0 -0
  23. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/control_plane.py +0 -0
  24. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/credentials.py +0 -0
  25. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/local_constitution.py +0 -0
  26. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/local_engine_runtime.py +0 -0
  27. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/local_paths.py +0 -0
  28. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/model_policy.py +0 -0
  29. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/runtime_manager.py +0 -0
  30. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli/settings.py +0 -0
  31. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli.egg-info/dependency_links.txt +0 -0
  32. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli.egg-info/entry_points.txt +0 -0
  33. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli.egg-info/requires.txt +0 -0
  34. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/refactorai_cli.egg-info/top_level.txt +0 -0
  35. {refactorai_cli-0.2.26 → refactorai_cli-0.2.28}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.2.26
3
+ Version: 0.2.28
4
4
  Summary: Local-first CLI for the refactor platform
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -53,6 +53,39 @@ python -m twine upload ./refactorai-cli/dist/*
53
53
  ## Install test (local)
54
54
 
55
55
  ```bash
56
- python -m pip install ./refactorai-cli/dist/refactorai_cli-0.2.26-py3-none-any.whl
56
+ python -m pip install ./refactorai-cli/dist/refactorai_cli-0.2.28-py3-none-any.whl
57
57
  refactor --version
58
58
  ```
59
+
60
+ ## Cloud BYOK setup (bring your own provider key)
61
+
62
+ Run cloud inference with your own provider credentials (no local runtime
63
+ required). The secret is read from your environment and encrypted server-side;
64
+ it is never written to disk or printed.
65
+
66
+ ```bash
67
+ # 1. Authenticate (developer key with the run:byok entitlement).
68
+ refactor login
69
+
70
+ # 2. Register your provider secret from an env var.
71
+ export OPENAI_API_KEY=sk-...
72
+ refactor cloud credentials set --provider openai --from-env OPENAI_API_KEY --label "my key"
73
+ # -> prints a credential_ref, e.g. cred_01J...
74
+
75
+ # 3. Point your project at cloud_byok in refactor.config:
76
+ # mode: cloud_byok
77
+ # provider: openai
78
+ # credential_ref: cred_01J...
79
+
80
+ # 4. Verify readiness, then run.
81
+ refactor doctor
82
+ refactor review .
83
+ refactor code . --apply
84
+ ```
85
+
86
+ Manage credentials:
87
+
88
+ ```bash
89
+ refactor cloud credentials list
90
+ refactor cloud credentials remove --credential-ref cred_01J...
91
+ ```
@@ -0,0 +1,80 @@
1
+ # refactorai-cli
2
+
3
+ Public CLI package for Refactor.
4
+
5
+ - PyPI package name: `refactorai-cli`
6
+ - Installed command: `refactor`
7
+ - Python module package: `refactorai_cli`
8
+
9
+ By default, the CLI targets `https://api.refactorai.codes`.
10
+ Use `REFACTOR_PLATFORM_URL` only when you need to override the control-plane URL
11
+ (for self-hosted or local development environments).
12
+
13
+ ## Local development install
14
+
15
+ From repository root:
16
+
17
+ ```bash
18
+ pip install -e refactor_core -e refactorai-cli
19
+ ```
20
+
21
+ ## Build
22
+
23
+ From repository root:
24
+
25
+ ```bash
26
+ python -m pip install --upgrade build twine
27
+ python -m build "./refactorai-cli"
28
+ ```
29
+
30
+ Artifacts are created in:
31
+
32
+ - `refactorai-cli/dist/*.whl`
33
+ - `refactorai-cli/dist/*.tar.gz`
34
+
35
+ ## Publish
36
+
37
+ ```bash
38
+ python -m twine check ./refactorai-cli/dist/*
39
+ python -m twine upload ./refactorai-cli/dist/*
40
+ ```
41
+
42
+ ## Install test (local)
43
+
44
+ ```bash
45
+ python -m pip install ./refactorai-cli/dist/refactorai_cli-0.2.28-py3-none-any.whl
46
+ refactor --version
47
+ ```
48
+
49
+ ## Cloud BYOK setup (bring your own provider key)
50
+
51
+ Run cloud inference with your own provider credentials (no local runtime
52
+ required). The secret is read from your environment and encrypted server-side;
53
+ it is never written to disk or printed.
54
+
55
+ ```bash
56
+ # 1. Authenticate (developer key with the run:byok entitlement).
57
+ refactor login
58
+
59
+ # 2. Register your provider secret from an env var.
60
+ export OPENAI_API_KEY=sk-...
61
+ refactor cloud credentials set --provider openai --from-env OPENAI_API_KEY --label "my key"
62
+ # -> prints a credential_ref, e.g. cred_01J...
63
+
64
+ # 3. Point your project at cloud_byok in refactor.config:
65
+ # mode: cloud_byok
66
+ # provider: openai
67
+ # credential_ref: cred_01J...
68
+
69
+ # 4. Verify readiness, then run.
70
+ refactor doctor
71
+ refactor review .
72
+ refactor code . --apply
73
+ ```
74
+
75
+ Manage credentials:
76
+
77
+ ```bash
78
+ refactor cloud credentials list
79
+ refactor cloud credentials remove --credential-ref cred_01J...
80
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "refactorai-cli"
3
- version = "0.2.26"
3
+ version = "0.2.28"
4
4
  description = "Local-first CLI for the refactor platform"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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.2.26"
8
+ __version__ = "0.2.28"
@@ -0,0 +1,176 @@
1
+ """`refactor cloud` command group for BYOK cloud credential management (R20).
2
+
3
+ Lets developers self-serve register/list/remove provider credentials used by
4
+ ``cloud_byok`` inference without hand-crafting API calls. Secrets are read from
5
+ the environment (never passed on the command line) and are envelope-encrypted
6
+ server-side; responses are always masked.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+
13
+ import httpx
14
+ import typer
15
+ from rich.console import Console
16
+ from rich.table import Table
17
+
18
+ from refactorai_cli.auth import AuthError, ensure_authenticated
19
+ from refactorai_cli.settings import platform_url
20
+
21
+ console = Console()
22
+
23
+ app = typer.Typer(
24
+ name="cloud",
25
+ help="Cloud (BYOK) credential management and helpers.",
26
+ no_args_is_help=True,
27
+ add_completion=False,
28
+ )
29
+
30
+ credentials_app = typer.Typer(
31
+ name="credentials",
32
+ help="Manage provider credentials for cloud_byok inference.",
33
+ no_args_is_help=True,
34
+ add_completion=False,
35
+ )
36
+ app.add_typer(credentials_app, name="credentials")
37
+
38
+ _CREDENTIALS_PATH = "/v1/inference/credentials"
39
+ _TIMEOUT = 20.0
40
+
41
+
42
+ def _auth_header() -> tuple[str, str]:
43
+ """Return ``(base_url, bearer_key)`` or exit with a clear auth error."""
44
+ try:
45
+ auth_ctx = ensure_authenticated()
46
+ except AuthError as exc:
47
+ console.print(f"[red]Not authenticated:[/red] {exc}")
48
+ raise typer.Exit(code=1) from exc
49
+ return platform_url(), auth_ctx.key.key
50
+
51
+
52
+ def _handle_error_status(response: httpx.Response) -> None:
53
+ """Map credential API error statuses to actionable CLI messages."""
54
+ if response.status_code < 400:
55
+ return
56
+ if response.status_code == 401:
57
+ console.print("[red]Unauthorized[/red]: developer key invalid or revoked. Run `refactor login`.")
58
+ elif response.status_code == 402:
59
+ console.print(
60
+ "[red]BYOK not enabled[/red] for this account (missing `run:byok`). "
61
+ "Contact your admin to enable BYOK cloud execution."
62
+ )
63
+ elif response.status_code == 404:
64
+ console.print("[red]Not found[/red]: credential does not exist or is not owned by this account.")
65
+ else:
66
+ detail = ""
67
+ try:
68
+ detail = str((response.json() or {}).get("detail", ""))
69
+ except Exception:
70
+ detail = response.text[:200]
71
+ console.print(f"[red]Request failed[/red] ({response.status_code}): {detail}")
72
+ raise typer.Exit(code=1)
73
+
74
+
75
+ @credentials_app.command("set")
76
+ def credentials_set(
77
+ provider: str = typer.Option(..., "--provider", help="Provider name, e.g. openai, anthropic."),
78
+ from_env: str = typer.Option(
79
+ ...,
80
+ "--from-env",
81
+ help="Environment variable holding the provider secret (value is read from your shell).",
82
+ ),
83
+ label: str = typer.Option("", "--label", help="Optional human-readable label for this credential."),
84
+ ) -> None:
85
+ """Register (or replace) a provider credential from an environment variable.
86
+
87
+ The secret value is read from ``--from-env`` and sent over TLS to be
88
+ encrypted server-side. It is never written to disk or shown on screen.
89
+ """
90
+ secret_value = os.environ.get(from_env, "").strip()
91
+ if not secret_value:
92
+ console.print(
93
+ f"[red]Environment variable ${{{from_env}}} is empty or unset.[/red] "
94
+ "Export your provider secret first, e.g. "
95
+ f"`export {from_env}=sk-...` then re-run."
96
+ )
97
+ raise typer.Exit(code=1)
98
+
99
+ base_url, key = _auth_header()
100
+ try:
101
+ response = httpx.post(
102
+ f"{base_url}{_CREDENTIALS_PATH}",
103
+ headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
104
+ json={"provider": provider, "secret_value": secret_value, "credential_label": label},
105
+ timeout=_TIMEOUT,
106
+ )
107
+ except httpx.HTTPError as exc:
108
+ console.print(f"[red]Could not reach platform:[/red] {exc}")
109
+ raise typer.Exit(code=1) from exc
110
+
111
+ _handle_error_status(response)
112
+ data = response.json() if response.content else {}
113
+ console.print(
114
+ f"[green]Credential registered[/green] for provider [bold]{data.get('provider', provider)}[/bold].\n"
115
+ f" credential_ref: [bold]{data.get('credential_ref', '?')}[/bold]\n"
116
+ f" masked_secret: {data.get('masked_secret', '****')}\n"
117
+ "Add the credential_ref to refactor.config under `credential_ref:` to use cloud_byok."
118
+ )
119
+
120
+
121
+ @credentials_app.command("list")
122
+ def credentials_list() -> None:
123
+ """List the developer's active BYOK provider credentials (masked)."""
124
+ base_url, key = _auth_header()
125
+ try:
126
+ response = httpx.get(
127
+ f"{base_url}{_CREDENTIALS_PATH}",
128
+ headers={"Authorization": f"Bearer {key}"},
129
+ timeout=_TIMEOUT,
130
+ )
131
+ except httpx.HTTPError as exc:
132
+ console.print(f"[red]Could not reach platform:[/red] {exc}")
133
+ raise typer.Exit(code=1) from exc
134
+
135
+ _handle_error_status(response)
136
+ data = response.json() if response.content else {}
137
+ rows = data.get("credentials", []) if isinstance(data, dict) else []
138
+ if not rows:
139
+ console.print("[dim]No cloud credentials registered.[/dim] Use `refactor cloud credentials set`.")
140
+ return
141
+
142
+ table = Table(title="Cloud BYOK credentials")
143
+ table.add_column("provider")
144
+ table.add_column("credential_ref")
145
+ table.add_column("label")
146
+ table.add_column("secret")
147
+ table.add_column("created")
148
+ for row in rows:
149
+ table.add_row(
150
+ str(row.get("provider", "")),
151
+ str(row.get("credential_ref", "")),
152
+ str(row.get("credential_label", "") or ""),
153
+ str(row.get("masked_secret", "****")),
154
+ str(row.get("created_at", "") or ""),
155
+ )
156
+ console.print(table)
157
+
158
+
159
+ @credentials_app.command("remove")
160
+ def credentials_remove(
161
+ credential_ref: str = typer.Option(..., "--credential-ref", help="Credential reference to deactivate."),
162
+ ) -> None:
163
+ """Deactivate (remove) a BYOK provider credential by reference."""
164
+ base_url, key = _auth_header()
165
+ try:
166
+ response = httpx.delete(
167
+ f"{base_url}{_CREDENTIALS_PATH}/{credential_ref}",
168
+ headers={"Authorization": f"Bearer {key}"},
169
+ timeout=_TIMEOUT,
170
+ )
171
+ except httpx.HTTPError as exc:
172
+ console.print(f"[red]Could not reach platform:[/red] {exc}")
173
+ raise typer.Exit(code=1) from exc
174
+
175
+ _handle_error_status(response)
176
+ console.print(f"[green]Removed[/green] credential [bold]{credential_ref}[/bold].")
@@ -7,6 +7,7 @@ generation and application.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import hashlib
10
11
  import os
11
12
  import json
12
13
  import re
@@ -578,8 +579,40 @@ def _require_auth() -> None:
578
579
  raise typer.Exit(code=1) from exc
579
580
 
580
581
 
582
+ def _cloud_execution_mode(constitution) -> str:
583
+ """Return the cloud execution mode for the project, or "" for local mode.
584
+
585
+ Recognizes both cloud modes (R20):
586
+ - ``cloud_managed``: platform-managed provider keys.
587
+ - ``cloud_byok``: developer-supplied provider credential (via credential_ref).
588
+ """
589
+ mode = str(constitution.get_setting("mode", "local") or "local").strip().lower()
590
+ return mode if mode in {"cloud_managed", "cloud_byok"} else ""
591
+
592
+
581
593
  def _is_cloud_mode(constitution) -> bool:
582
- return str(constitution.get_setting("mode", "local") or "local").strip().lower() == "cloud_managed"
594
+ return bool(_cloud_execution_mode(constitution))
595
+
596
+
597
+ def _stable_idempotency_key(
598
+ *,
599
+ account_id: str,
600
+ stage: str,
601
+ constitution_hash: str,
602
+ target: str,
603
+ max_alerts: int,
604
+ execution_mode: str,
605
+ ) -> str:
606
+ """Deterministic idempotency key so retries replay instead of re-charging (C10).
607
+
608
+ The key MUST change when inputs change (target/constitution/stage/mode) so
609
+ distinct runs are not incorrectly deduplicated, and MUST be stable across
610
+ retries of the same logical run so managed quota is not double-charged.
611
+ """
612
+ seed = "|".join(
613
+ [account_id, execution_mode, stage, constitution_hash, str(target), str(int(max_alerts))]
614
+ )
615
+ return hashlib.sha256(seed.encode("utf-8")).hexdigest()
583
616
 
584
617
 
585
618
  def _cloud_inference(
@@ -589,6 +622,7 @@ def _cloud_inference(
589
622
  constitution,
590
623
  target: str,
591
624
  model_hint: str | None = None,
625
+ findings: list | None = None,
592
626
  ) -> RunArtifact:
593
627
  max_alerts = int(constitution.get_setting("max_alerts_per_run", 5) or 5)
594
628
  gate_mode = str(constitution.get_setting("gate_mode", "strict") or "strict")
@@ -600,35 +634,72 @@ def _cloud_inference(
600
634
  if stage == "review":
601
635
  system_prompt, user_prompt = build_review_prompt(constitution, documents, max_alerts)
602
636
  else:
603
- system_prompt, user_prompt = build_refactor_prompt(constitution, documents, [], max_alerts)
637
+ # Feed prior review findings into the refactor prompt so cloud `code`
638
+ # generates patches targeting the same issues (parity with local RR).
639
+ system_prompt, user_prompt = build_refactor_prompt(
640
+ constitution, documents, list(findings or []), max_alerts
641
+ )
604
642
 
643
+ execution_mode = _cloud_execution_mode(constitution) or "cloud_managed"
605
644
  auth_ctx = ensure_authenticated(project_root)
606
645
  payload = {
607
646
  "stage": stage,
647
+ "execution_mode": execution_mode,
608
648
  "constitution_hash": constitution.content_hash,
609
649
  "system_prompt": system_prompt,
610
650
  "user_prompt": user_prompt,
611
651
  "max_alerts": max_alerts,
612
652
  "model_hint": model_hint or str(constitution.get_setting("model_id", "managed-default") or "managed-default"),
613
653
  }
654
+
655
+ if execution_mode == "cloud_byok":
656
+ credential_ref = str(constitution.get_setting("credential_ref", "") or "").strip()
657
+ provider_name = str(constitution.get_setting("provider", "") or "").strip()
658
+ if not credential_ref:
659
+ raise RuntimeError(
660
+ "cloud_byok requires a `credential_ref` in refactor.config. "
661
+ "Register one with `refactor cloud credentials set` and copy its ref."
662
+ )
663
+ payload["credential_ref"] = credential_ref
664
+ if provider_name and provider_name.lower() != "cloud":
665
+ payload["provider"] = provider_name
666
+
667
+ idempotency_key = _stable_idempotency_key(
668
+ account_id=auth_ctx.account_id,
669
+ stage=stage,
670
+ constitution_hash=constitution.content_hash,
671
+ target=target,
672
+ max_alerts=max_alerts,
673
+ execution_mode=execution_mode,
674
+ )
614
675
  try:
615
676
  response = httpx.post(
616
677
  f"{platform_url()}/v1/inference/refactor",
617
678
  headers={
618
679
  "Authorization": f"Bearer {auth_ctx.key.key}",
619
680
  "Content-Type": "application/json",
620
- "Idempotency-Key": uuid.uuid4().hex,
681
+ "Idempotency-Key": idempotency_key,
621
682
  },
622
683
  json=payload,
623
684
  timeout=45.0,
624
685
  )
625
686
  except httpx.HTTPError as exc:
626
- raise RuntimeError(f"Could not reach managed inference service: {exc}") from exc
687
+ raise RuntimeError(f"Could not reach cloud inference service: {exc}") from exc
627
688
 
628
689
  if response.status_code == 401:
629
690
  raise RuntimeError("Cloud inference unauthorized (developer key invalid or revoked).")
630
691
  if response.status_code == 402:
692
+ if execution_mode == "cloud_byok":
693
+ raise RuntimeError(
694
+ "Cloud BYOK is not enabled for this account (missing `run:byok`). "
695
+ "Contact your admin to enable BYOK cloud execution."
696
+ )
631
697
  raise RuntimeError("Cloud inference requires paid tier (`mode: cloud_managed`).")
698
+ if response.status_code == 404 and execution_mode == "cloud_byok":
699
+ raise RuntimeError(
700
+ "BYOK credential not found or not owned by this account. "
701
+ "Check `refactor cloud credentials list` and your `credential_ref`."
702
+ )
632
703
  if response.status_code == 429:
633
704
  raise RuntimeError("Cloud inference quota exhausted or rate-limited.")
634
705
  if response.status_code >= 400:
@@ -1926,6 +1997,10 @@ def code(
1926
1997
  constitution, project_root=project_root, provider_name=provider
1927
1998
  )
1928
1999
 
2000
+ # Cloud refactor generation reuses the review findings as prompt context;
2001
+ # track them so they flow into the cloud refactor stage below (C2).
2002
+ cloud_review_findings: list[Finding] = []
2003
+
1929
2004
  # If no pending refactor requests exist, run review first to create them.
1930
2005
  requests = pending_requests(project_root)
1931
2006
  if not requests:
@@ -1938,6 +2013,7 @@ def code(
1938
2013
  target=target,
1939
2014
  model_hint=provider,
1940
2015
  )
2016
+ cloud_review_findings = list(review_artifact.findings or [])
1941
2017
  else:
1942
2018
  with console.status("[cyan]review[/cyan] analyzing...", spinner="dots") as status:
1943
2019
  review_progress = _agent_progress_logger(
@@ -2006,11 +2082,60 @@ def code(
2006
2082
  return
2007
2083
 
2008
2084
  if cloud:
2085
+ # Cloud modes generate patches via the platform inference API rather
2086
+ # than the local RR interactive loop. Reuse prior review findings as
2087
+ # prompt context; fall back to the latest stored review findings when
2088
+ # requests were carried over from an earlier run.
2089
+ if not cloud_review_findings:
2090
+ existing_dir = head_run_dir(project_root)
2091
+ if existing_dir is not None:
2092
+ try:
2093
+ cloud_review_findings = [
2094
+ Finding.model_validate(item) for item in (read_findings(existing_dir) or [])
2095
+ ]
2096
+ except Exception:
2097
+ cloud_review_findings = []
2098
+ with console.status("[cyan]code[/cyan] generating patches (cloud)...", spinner="dots"):
2099
+ artifact = _cloud_inference(
2100
+ stage="refactor",
2101
+ project_root=project_root,
2102
+ constitution=constitution,
2103
+ target=target,
2104
+ model_hint=provider,
2105
+ findings=cloud_review_findings,
2106
+ )
2107
+ cloud_store = create_run(project_root, run_id=artifact.run_id)
2108
+ cloud_store.write_artifact(artifact, target=target, status="completed")
2109
+
2110
+ _print_rr_summary(artifact)
2111
+ _print_patches(artifact.patches)
2112
+ gate_color = "green" if artifact.gate.status == "passed" else "red"
2009
2113
  console.print(
2010
- f"[yellow]Created {len(requests)} refactor request(s).[/yellow] "
2011
- "The interactive apply-and-verify loop runs against a local provider; "
2012
- "configure a local provider/key to work through them with `refactor code`."
2114
+ f"\n[bold]result[/bold]: [{gate_color}]{artifact.gate.status}[/{gate_color}] "
2115
+ f"[dim]model={artifact.model_id} mode={_cloud_execution_mode(constitution)} "
2116
+ f"run={artifact.run_id}[/dim]"
2013
2117
  )
2118
+
2119
+ if apply:
2120
+ gate_mode = str(constitution.get_setting("gate_mode", "strict") or "strict")
2121
+ diffs = [p.diff_text for p in (artifact.patches or [])]
2122
+ if not diffs:
2123
+ console.print("[dim]No patches to apply.[/dim]")
2124
+ else:
2125
+ _apply_diffs(
2126
+ project_root,
2127
+ cloud_store.run_dir,
2128
+ diffs,
2129
+ artifact.gate.status,
2130
+ gate_mode,
2131
+ force,
2132
+ project_config,
2133
+ )
2134
+ elif artifact.patches:
2135
+ console.print(
2136
+ "[dim]Review patches with `refactor diff`, then apply with "
2137
+ "`refactor code --apply` or `refactor apply`.[/dim]"
2138
+ )
2014
2139
  return
2015
2140
 
2016
2141
  with console.status("[cyan]code[/cyan] running...", spinner="dots") as status:
@@ -2030,6 +2155,9 @@ def code(
2030
2155
  on_rr_failure=on_rr_failure,
2031
2156
  equivalence_mode=equivalence,
2032
2157
  )
2158
+ except typer.Exit:
2159
+ # Intentional exits (e.g. gate-blocked apply) keep their own code/message.
2160
+ raise
2033
2161
  except Exception as exc:
2034
2162
  console.print(f"[red]Refactor failed:[/red] {_sanitize_runtime_error_message(exc)}")
2035
2163
  raise typer.Exit(code=1) from exc
@@ -2912,6 +3040,92 @@ def _missing_requirements(provider_name: str) -> list[str]:
2912
3040
  return missing
2913
3041
 
2914
3042
 
3043
+ def _cloud_doctor(
3044
+ *,
3045
+ project_root: Path,
3046
+ constitution,
3047
+ execution_mode: str,
3048
+ skip_ping: bool,
3049
+ ) -> None:
3050
+ """Cloud-mode readiness checks (C7): auth, entitlement, endpoint, credential.
3051
+
3052
+ Cloud modes don't require a local provider or runtime, so this path checks
3053
+ the control-plane identity and inference endpoint reachability instead of
3054
+ resolving/pinging a local provider.
3055
+ """
3056
+ console.print(f"[bold]project[/bold]: {project_root}")
3057
+ console.print(f"[bold]mode[/bold]: {execution_mode}")
3058
+
3059
+ try:
3060
+ auth_ctx = ensure_authenticated(project_root)
3061
+ cache_note = " (cached)" if auth_ctx.cached else ""
3062
+ console.print(f"[green]developer key[/green]: OK account={auth_ctx.account_id}{cache_note}")
3063
+ except AuthError as exc:
3064
+ console.print(f"[red]developer key[/red]: {exc}")
3065
+ console.print("[dim]Run `refactor login` or set REFACTOR_API_KEY.[/dim]")
3066
+ raise typer.Exit(code=1) from exc
3067
+
3068
+ # BYOK requires a credential_ref in config plus a matching server-side
3069
+ # credential owned by this account.
3070
+ credential_ref = ""
3071
+ if execution_mode == "cloud_byok":
3072
+ credential_ref = str(constitution.get_setting("credential_ref", "") or "").strip()
3073
+ if not credential_ref:
3074
+ console.print(
3075
+ "[red]cloud_byok[/red]: no `credential_ref` in refactor.config. "
3076
+ "Register one with `refactor cloud credentials set` and add its ref."
3077
+ )
3078
+ raise typer.Exit(code=1)
3079
+ console.print(f"[green]credential_ref[/green]: {credential_ref}")
3080
+ provider_name = str(constitution.get_setting("provider", "") or "").strip()
3081
+ if provider_name and provider_name.lower() != "cloud":
3082
+ console.print(f"[bold]provider[/bold]: {provider_name}")
3083
+
3084
+ if skip_ping:
3085
+ console.print("[dim]Skipping cloud endpoint reachability check (--skip-ping).[/dim]")
3086
+ return
3087
+
3088
+ # Verify the BYOK credential exists and is owned by this account, and that
3089
+ # the credential endpoint is reachable.
3090
+ if execution_mode == "cloud_byok":
3091
+ try:
3092
+ response = httpx.get(
3093
+ f"{platform_url()}/v1/inference/credentials",
3094
+ headers={"Authorization": f"Bearer {auth_ctx.key.key}"},
3095
+ timeout=20.0,
3096
+ )
3097
+ except httpx.HTTPError as exc:
3098
+ console.print(f"[red]cloud endpoint[/red]: unreachable ({exc}).")
3099
+ raise typer.Exit(code=1) from exc
3100
+ if response.status_code == 402:
3101
+ console.print(
3102
+ "[red]entitlement[/red]: BYOK not enabled for this account (missing `run:byok`). "
3103
+ "Contact your admin to enable BYOK cloud execution."
3104
+ )
3105
+ raise typer.Exit(code=1)
3106
+ if response.status_code == 401:
3107
+ console.print("[red]developer key[/red]: unauthorized (invalid or revoked). Run `refactor login`.")
3108
+ raise typer.Exit(code=1)
3109
+ if response.status_code >= 400:
3110
+ console.print(f"[red]cloud endpoint[/red]: credential check failed ({response.status_code}).")
3111
+ raise typer.Exit(code=1)
3112
+ data = response.json() if response.content else {}
3113
+ refs = {str(row.get("credential_ref", "")) for row in (data.get("credentials", []) or [])}
3114
+ if credential_ref not in refs:
3115
+ console.print(
3116
+ f"[red]credential[/red]: `{credential_ref}` not found for this account. "
3117
+ "Check `refactor cloud credentials list`."
3118
+ )
3119
+ raise typer.Exit(code=1)
3120
+ console.print("[green]credential[/green]: found and owned by this account.")
3121
+ console.print("[green]cloud endpoint[/green]: reachable.")
3122
+ else:
3123
+ console.print(
3124
+ "[dim]cloud_managed uses platform-managed provider credentials; "
3125
+ "run `refactor review` to exercise the endpoint.[/dim]"
3126
+ )
3127
+
3128
+
2915
3129
  def doctor(
2916
3130
  provider: str | None = typer.Option(
2917
3131
  None,
@@ -2946,6 +3160,16 @@ def doctor(
2946
3160
  _report_sandbox(project_root, project_config)
2947
3161
  return
2948
3162
 
3163
+ execution_mode = _cloud_execution_mode(constitution)
3164
+ if execution_mode:
3165
+ _cloud_doctor(
3166
+ project_root=project_root,
3167
+ constitution=constitution,
3168
+ execution_mode=execution_mode,
3169
+ skip_ping=skip_ping,
3170
+ )
3171
+ return
3172
+
2949
3173
  mode = str(constitution.get_setting("mode", "local") or "local")
2950
3174
  provider_name = resolve_provider_name(constitution, provider_name=provider)
2951
3175