refactorai-cli 0.3.3__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.3 → refactorai_cli-0.3.4}/PKG-INFO +3 -3
  2. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/README.md +1 -1
  3. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/pyproject.toml +2 -2
  4. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/__init__.py +1 -1
  5. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/cloud_rr.py +201 -2
  6. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/commands/run_cmds.py +3 -3
  7. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli.egg-info/PKG-INFO +3 -3
  8. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli.egg-info/requires.txt +1 -1
  9. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/auth.py +0 -0
  10. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/client.py +0 -0
  11. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/commands/__init__.py +0 -0
  12. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/commands/auth_cmds.py +0 -0
  13. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/commands/cloud_cmds.py +0 -0
  14. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/commands/engine_cmds.py +0 -0
  15. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/commands/model_cmds.py +0 -0
  16. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/commands/rules_cmds.py +0 -0
  17. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/commands/runtime_cmds.py +0 -0
  18. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
  19. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/commands/setup_cmds.py +0 -0
  20. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/control_plane.py +0 -0
  21. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/credentials.py +0 -0
  22. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/local_constitution.py +0 -0
  23. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/local_engine_runtime.py +0 -0
  24. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/local_paths.py +0 -0
  25. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/main.py +0 -0
  26. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/model_policy.py +0 -0
  27. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/runtime_manager.py +0 -0
  28. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/settings.py +0 -0
  29. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli/setup_flow.py +0 -0
  30. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli.egg-info/SOURCES.txt +0 -0
  31. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli.egg-info/dependency_links.txt +0 -0
  32. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli.egg-info/entry_points.txt +0 -0
  33. {refactorai_cli-0.3.3 → refactorai_cli-0.3.4}/refactorai_cli.egg-info/top_level.txt +0 -0
  34. {refactorai_cli-0.3.3 → 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.3
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.3
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.3-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.3-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.3"
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.3",
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.3"
8
+ __version__ = "0.3.4"
@@ -23,6 +23,9 @@ this design fixes. ``tests/test_cloud_rr_client_boundary.py`` enforces this.
23
23
  from __future__ import annotations
24
24
 
25
25
  import hashlib
26
+ import re
27
+ import shlex
28
+ import shutil
26
29
  import subprocess
27
30
  from pathlib import Path
28
31
  from typing import Callable
@@ -48,11 +51,33 @@ from refactor_core.refactor_requests import (
48
51
  save_request,
49
52
  )
50
53
  from refactor_core.store import create_run
51
- from refactor_core.testcmd import resolve_test_command
54
+ from refactor_core.testcmd import _SKIP_DIRS, discover_test_command, resolve_test_command
52
55
 
53
56
  from refactorai_cli.auth import ensure_authenticated
54
57
  from refactorai_cli.settings import platform_url
55
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
+
56
81
 
57
82
  class CloudRRError(RuntimeError):
58
83
  """Raised when the cloud RR session cannot proceed."""
@@ -98,6 +123,170 @@ def _run_test(project_root: Path, test_command: str | None) -> tuple[bool, str]:
98
123
  return False, (proc.stdout or proc.stderr or "")[-500:]
99
124
 
100
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
+
101
290
  class _Session:
102
291
  """Transport for the RR session endpoints with status-code error mapping."""
103
292
 
@@ -200,7 +389,15 @@ def run_cloud_refactor_requests(
200
389
 
201
390
  execution_mode = str(constitution.get_setting("mode", "cloud_managed") or "cloud_managed")
202
391
  auth_ctx = ensure_authenticated(project_root)
203
- test_command = resolve_test_command(project_root, config)
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
+ )
204
401
 
205
402
  if requests is None:
206
403
  requests = pending_requests(project_root)
@@ -217,6 +414,8 @@ def run_cloud_refactor_requests(
217
414
  outcomes: list[dict] = []
218
415
  counts = {"applied": 0, "denied": 0, "failed": 0, "deferred": 0}
219
416
  notes: list[str] = []
417
+ if testcmd_note:
418
+ notes.append(testcmd_note)
220
419
  stopped = False
221
420
  stop_reason = ""
222
421
  model_id = resolved_model
@@ -1956,7 +1956,7 @@ def code(
1956
1956
  help="Skip interactive issue approvals in agentic code mode.",
1957
1957
  ),
1958
1958
  on_rr_failure: str = typer.Option(
1959
- "stop",
1959
+ "continue",
1960
1960
  "--on-rr-failure",
1961
1961
  help="RR failure behavior: stop | continue | defer.",
1962
1962
  ),
@@ -2227,7 +2227,7 @@ def requests(
2227
2227
  help="Auto-approve retry-deferred proposals.",
2228
2228
  ),
2229
2229
  on_rr_failure: str = typer.Option(
2230
- "stop",
2230
+ "continue",
2231
2231
  "--on-rr-failure",
2232
2232
  help="RR failure behavior for retry: stop | continue | defer.",
2233
2233
  ),
@@ -3484,7 +3484,7 @@ def _report_structural_readiness(project_root: Path, config) -> None:
3484
3484
  if not isinstance(structural, dict):
3485
3485
  structural = {}
3486
3486
  eq_mode = str(structural.get("equivalence_mode", "strict") or "strict")
3487
- rr_policy = str(structural.get("on_rr_failure", "stop") or "stop")
3487
+ rr_policy = str(structural.get("on_rr_failure", "continue") or "continue")
3488
3488
  rewrite_mode = str(structural.get("reference_rewrite", "auto") or "auto")
3489
3489
  langs = structural.get("languages", [])
3490
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.3
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.3
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.3-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
 
@@ -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.3
5
+ refactorai-core>=3.0.4
File without changes