refactorai-cli 0.7.12__tar.gz → 0.7.14__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 (55) hide show
  1. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/PKG-INFO +51 -2
  2. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/README.md +49 -0
  3. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/pyproject.toml +2 -2
  4. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/__init__.py +1 -1
  5. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/run_cmds.py +22 -10
  6. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/runtime_proxy_cmds.py +25 -0
  7. refactorai_cli-0.7.14/refactorai_cli/mode_sync.py +171 -0
  8. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/setup_flow.py +19 -35
  9. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli.egg-info/PKG-INFO +51 -2
  10. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli.egg-info/requires.txt +1 -1
  11. refactorai_cli-0.7.12/refactorai_cli/mode_sync.py +0 -140
  12. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/auth.py +0 -0
  13. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/client.py +0 -0
  14. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/cloud_rr.py +0 -0
  15. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/__init__.py +0 -0
  16. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/account_cmds.py +0 -0
  17. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/auth_cmds.py +0 -0
  18. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/branch_cmds.py +0 -0
  19. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/cloud_cmds.py +0 -0
  20. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/engine_cmds.py +0 -0
  21. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/hook_cmds.py +0 -0
  22. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/model_cmds.py +0 -0
  23. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/policy_cmds.py +0 -0
  24. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/pre_push_cmds.py +0 -0
  25. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/request_cmds.py +0 -0
  26. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/rules_cmds.py +0 -0
  27. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/runtime_cmds.py +0 -0
  28. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/setup_cmds.py +0 -0
  29. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/toolchains_cmds.py +0 -0
  30. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/watch_cmds.py +0 -0
  31. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commands/workspace_cmds.py +0 -0
  32. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commit_queue.py +0 -0
  33. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/commit_telemetry.py +0 -0
  34. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/control_plane.py +0 -0
  35. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/credentials.py +0 -0
  36. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/dotenv_loader.py +0 -0
  37. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/git_scope.py +0 -0
  38. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/local_constitution.py +0 -0
  39. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/local_engine_runtime.py +0 -0
  40. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/local_paths.py +0 -0
  41. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/main.py +0 -0
  42. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/model_policy.py +0 -0
  43. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/pre_push_gate.py +0 -0
  44. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/refactor_branch_store.py +0 -0
  45. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/review_runner.py +0 -0
  46. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/runtime_manager.py +0 -0
  47. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/settings.py +0 -0
  48. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/watch_ledger.py +0 -0
  49. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/watch_state.py +0 -0
  50. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli/watch_supervisor.py +0 -0
  51. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli.egg-info/SOURCES.txt +0 -0
  52. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli.egg-info/dependency_links.txt +0 -0
  53. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli.egg-info/entry_points.txt +0 -0
  54. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/refactorai_cli.egg-info/top_level.txt +0 -0
  55. {refactorai_cli-0.7.12 → refactorai_cli-0.7.14}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.7.12
3
+ Version: 0.7.14
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.22
11
+ Requires-Dist: refactorai-core>=3.2.23
12
12
 
13
13
  # refactorai-cli
14
14
 
@@ -66,6 +66,55 @@ the managed host toolchain when no container runtime is available. `refactor
66
66
  doctor` reports the selected environment (and `refactor doctor --sandbox` shows
67
67
  its full state), so a run and diagnostics always agree.
68
68
 
69
+ ## Customizing refactor.config
70
+
71
+ `refactor init` writes a **bare-minimum** `refactor.config`: just `version`,
72
+ `project_id`, and a CLI-managed block seeded to `local_heuristic`. A normal
73
+ developer never edits this file by hand — pick the mode (and provider keys, if
74
+ any) in the web UI or via `refactor setup --mode <variant>`.
75
+
76
+ ### The managed block (single source of truth for the mode)
77
+
78
+ ```
79
+ # >>> refactor managed (synced from server; do not edit) >>>
80
+ execution_variant: cloud_byok
81
+ provider: openai
82
+ model_id: gpt-4.1-mini
83
+ credential_ref: cred_01J...
84
+ # <<< refactor managed <<<
85
+ ```
86
+
87
+ - Owned by the CLI. `refactor setup` (terminal) writes it immediately; a change
88
+ made in the web UI is pulled into this block on the next `refactor doctor` /
89
+ `refactor watch` / `refactor setup` (**server-wins**).
90
+ - Only these keys live here: `execution_variant`, `provider`, `model_id`,
91
+ `base_url` (local_model), `credential_ref`. **Secrets are never written** —
92
+ keep provider keys in the environment and reference them as `${ENV_VAR}`.
93
+
94
+ ### What each mode needs
95
+
96
+ | variant | you provide |
97
+ | ---------------- | ------------------------------------------------------- |
98
+ | `local_heuristic`| nothing |
99
+ | `local_model` | nothing (setup writes the Ollama provider/model block) |
100
+ | `local_byok` | provider API key in the env (`${VAR}`); provider/model via UI/setup |
101
+ | `local_managed` | nothing |
102
+ | `cloud_byok` | provider API key in env (or a UI credential); provider/model via UI/setup |
103
+ | `cloud_managed` | nothing |
104
+
105
+ ### Advanced knobs and precedence
106
+
107
+ Advanced settings (`sandbox`, `verification`, `intelligence`, `compliance`,
108
+ `watch`, `timeout`, `exclude`, `model_context_overrides`, …) are **not** written
109
+ by `init`; add only the ones you need anywhere **outside** the managed block.
110
+ `refactor init --full` writes the fully-annotated template with every knob if you
111
+ prefer to start from that.
112
+
113
+ Resolution precedence: CLI flags → environment (`provider_key`,
114
+ `REFACTOR_PROVIDER`) → the synced managed block → code defaults. A key you place
115
+ outside the managed block (later in the file) also takes precedence over the
116
+ block, so power users can pin values while normal syncing continues.
117
+
69
118
  ## Local development install
70
119
 
71
120
  From repository root:
@@ -54,6 +54,55 @@ the managed host toolchain when no container runtime is available. `refactor
54
54
  doctor` reports the selected environment (and `refactor doctor --sandbox` shows
55
55
  its full state), so a run and diagnostics always agree.
56
56
 
57
+ ## Customizing refactor.config
58
+
59
+ `refactor init` writes a **bare-minimum** `refactor.config`: just `version`,
60
+ `project_id`, and a CLI-managed block seeded to `local_heuristic`. A normal
61
+ developer never edits this file by hand — pick the mode (and provider keys, if
62
+ any) in the web UI or via `refactor setup --mode <variant>`.
63
+
64
+ ### The managed block (single source of truth for the mode)
65
+
66
+ ```
67
+ # >>> refactor managed (synced from server; do not edit) >>>
68
+ execution_variant: cloud_byok
69
+ provider: openai
70
+ model_id: gpt-4.1-mini
71
+ credential_ref: cred_01J...
72
+ # <<< refactor managed <<<
73
+ ```
74
+
75
+ - Owned by the CLI. `refactor setup` (terminal) writes it immediately; a change
76
+ made in the web UI is pulled into this block on the next `refactor doctor` /
77
+ `refactor watch` / `refactor setup` (**server-wins**).
78
+ - Only these keys live here: `execution_variant`, `provider`, `model_id`,
79
+ `base_url` (local_model), `credential_ref`. **Secrets are never written** —
80
+ keep provider keys in the environment and reference them as `${ENV_VAR}`.
81
+
82
+ ### What each mode needs
83
+
84
+ | variant | you provide |
85
+ | ---------------- | ------------------------------------------------------- |
86
+ | `local_heuristic`| nothing |
87
+ | `local_model` | nothing (setup writes the Ollama provider/model block) |
88
+ | `local_byok` | provider API key in the env (`${VAR}`); provider/model via UI/setup |
89
+ | `local_managed` | nothing |
90
+ | `cloud_byok` | provider API key in env (or a UI credential); provider/model via UI/setup |
91
+ | `cloud_managed` | nothing |
92
+
93
+ ### Advanced knobs and precedence
94
+
95
+ Advanced settings (`sandbox`, `verification`, `intelligence`, `compliance`,
96
+ `watch`, `timeout`, `exclude`, `model_context_overrides`, …) are **not** written
97
+ by `init`; add only the ones you need anywhere **outside** the managed block.
98
+ `refactor init --full` writes the fully-annotated template with every knob if you
99
+ prefer to start from that.
100
+
101
+ Resolution precedence: CLI flags → environment (`provider_key`,
102
+ `REFACTOR_PROVIDER`) → the synced managed block → code defaults. A key you place
103
+ outside the managed block (later in the file) also takes precedence over the
104
+ block, so power users can pin values while normal syncing continues.
105
+
57
106
  ## Local development install
58
107
 
59
108
  From repository root:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "refactorai-cli"
3
- version = "0.7.12"
3
+ version = "0.7.14"
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.22",
15
+ "refactorai-core>=3.2.23",
16
16
  ]
17
17
 
18
18
  [project.scripts]
@@ -5,4 +5,4 @@ the shared `refactor_core` pipeline from a project folder while staying
5
5
  authenticated to the hosted platform via a developer key.
6
6
  """
7
7
 
8
- __version__ = "0.7.12"
8
+ __version__ = "0.7.14"
@@ -30,6 +30,7 @@ from refactor_core.constitution import (
30
30
  CONSTITUTION_FILENAME,
31
31
  DEFAULT_CONFIG,
32
32
  DEFAULT_CONSTITUTION,
33
+ MINIMAL_CONFIG,
33
34
  find_project_files,
34
35
  has_raw_secret_literal,
35
36
  load_config,
@@ -158,17 +159,22 @@ def _sandbox_runtime():
158
159
  return _sr
159
160
 
160
161
 
161
- def _default_config_for_init() -> tuple[str, str]:
162
- """Return the config `init` writes: always the ``local_heuristic`` default.
162
+ def _default_config_for_init(full: bool = False) -> tuple[str, str]:
163
+ """Return the config `init` writes.
163
164
 
164
- Mode selection is a *setup*-time concern (docs/46): after ``init`` the
165
- developer picks the execution variant in the web UI or via ``refactor setup
166
- --mode``, which persists it to ``refactor.config`` (and the server project).
167
- ``init`` therefore never derives the mode from prior setup-backend state --
168
- that legacy coupling produced configs whose variant disagreed with the
169
- server's default and is intentionally removed.
165
+ By default this is the bare-minimum template (docs/46): ``version`` +
166
+ the CLI-managed block seeded to ``local_heuristic``. A normal developer never
167
+ hand-edits it -- they pick a mode in the web UI or via ``refactor setup
168
+ --mode`` and the managed block is filled in by the CLI. ``--full`` writes the
169
+ fully-annotated template with every advanced knob for power users.
170
+
171
+ Mode selection is a *setup*-time concern: ``init`` never derives the mode
172
+ from prior setup-backend state (that legacy coupling produced configs whose
173
+ variant disagreed with the server default and is intentionally removed).
170
174
  """
171
- return DEFAULT_CONFIG, "default"
175
+ if full:
176
+ return DEFAULT_CONFIG, "full"
177
+ return MINIMAL_CONFIG, "default"
172
178
 
173
179
  _SEVERITY_STYLE = {
174
180
  "critical": "bold red",
@@ -1356,6 +1362,12 @@ def init(
1356
1362
  "--force",
1357
1363
  help=f"Overwrite existing {CONSTITUTION_FILENAME} and {CONFIG_FILENAME}.",
1358
1364
  ),
1365
+ full: bool = typer.Option(
1366
+ False,
1367
+ "--full",
1368
+ hidden=True,
1369
+ help="Write the fully-annotated refactor.config template (power users).",
1370
+ ),
1359
1371
  ) -> None:
1360
1372
  """Create `refactor.consti` + `refactor.config` and register the project.
1361
1373
 
@@ -1422,7 +1434,7 @@ def init(
1422
1434
  raise typer.Exit(code=1)
1423
1435
 
1424
1436
  consti_target.write_text(DEFAULT_CONSTITUTION, encoding="utf-8")
1425
- config_text, _config_profile = _default_config_for_init()
1437
+ config_text, _config_profile = _default_config_for_init(full=full)
1426
1438
  config_text = _inject_project_id(config_text, project_id)
1427
1439
  config_target.write_text(config_text, encoding="utf-8")
1428
1440
  gitignore_updated = _ensure_refactor_gitignore(project_root)
@@ -79,6 +79,30 @@ def _engine_runs_on_server() -> bool:
79
79
  return engine_is_remote(_project_execution_mode())
80
80
 
81
81
 
82
+ def _sync_mode_from_server_if_possible(command_name: str | None = None) -> None:
83
+ """Best-effort server->local mode sync before routing.
84
+
85
+ Fixes the stale-mode chicken-and-egg: when a developer changes the mode in
86
+ the web UI, the local ``refactor.config`` may still point at the previous
87
+ variant. If dispatch reads that stale local value first, commands can route
88
+ to the wrong engine path (for example ``doctor`` going to the runtime path
89
+ instead of the local CLI cloud path), so the sync never runs and the local
90
+ mode never updates.
91
+
92
+ This helper is intentionally silent and fail-open (offline/auth issues are
93
+ ignored) so command routing remains robust.
94
+ """
95
+ try:
96
+ from refactorai_cli.mode_sync import sync_execution_variant_from_server
97
+
98
+ emit = None
99
+ if command_name == "doctor":
100
+ emit = lambda m: console.print(f"[dim]{m}[/dim]")
101
+ sync_execution_variant_from_server(Path.cwd(), emit=emit)
102
+ except Exception:
103
+ return
104
+
105
+
82
106
  def _active_runtime_artifact() -> Path:
83
107
  status = runtime_status()
84
108
  version = str(status.get("active_version") or "").strip()
@@ -181,6 +205,7 @@ def _dispatch(command_name: str, passthrough_args: list[str]) -> None:
181
205
  host-engine variants (``local_model`` / ``local_heuristic``) execute via the
182
206
  downloaded runtime artifact.
183
207
  """
208
+ _sync_mode_from_server_if_possible(command_name)
184
209
  if _engine_runs_on_server():
185
210
  _delegate_to_run_cmds(command_name, passthrough_args)
186
211
  return
@@ -0,0 +1,171 @@
1
+ """Execution-mode sync between the server project and local ``refactor.config``.
2
+
3
+ The web UI sets a project's ``execution_mode`` on the server (JWT). The CLI
4
+ authenticates with a developer key and treats the server as the source of truth
5
+ (server-wins): ``setup`` / ``doctor`` / ``watch`` pull the server mode and write
6
+ it into the local ``refactor.config`` so a UI change reflects in terminal runs.
7
+ ``refactor setup --mode`` also pushes the chosen variant up so both agree.
8
+
9
+ All operations are best-effort: offline / unauthenticated / unbound projects are
10
+ a silent no-op so they never break the surrounding command.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+ from typing import Callable
17
+
18
+ from refactor_core.constitution import (
19
+ CONFIG_FILENAME,
20
+ MANAGED_KEYS,
21
+ find_project_files,
22
+ load_config,
23
+ upsert_managed_block,
24
+ )
25
+ from refactor_core.execution_variant import normalize_execution_variant
26
+ from refactor_core.store import resolve_project_id
27
+
28
+ from refactorai_cli.client import PlatformClient
29
+ from refactorai_cli.credentials import resolve_developer_key
30
+
31
+
32
+ def _local_config_path(project_root: Path) -> Path | None:
33
+ files = find_project_files(project_root)
34
+ if files:
35
+ return Path(files[1])
36
+ candidate = Path(project_root) / CONFIG_FILENAME
37
+ return candidate if candidate.is_file() else None
38
+
39
+
40
+ def _current_managed_values(project_root: Path) -> dict:
41
+ """Current values of the managed keys as parsed from refactor.config."""
42
+ config_path = _local_config_path(project_root)
43
+ if config_path is None:
44
+ return {}
45
+ try:
46
+ settings = getattr(load_config(config_path), "settings", {}) or {}
47
+ except Exception:
48
+ return {}
49
+ values: dict = {}
50
+ for key in MANAGED_KEYS:
51
+ raw = settings.get(key)
52
+ if raw is None:
53
+ continue
54
+ text = str(raw).strip()
55
+ if text:
56
+ values[key] = text
57
+ return values
58
+
59
+
60
+ def _desired_managed_fields(record: dict, current: dict, server_variant: str) -> dict:
61
+ """Compute the managed-block fields from the server record (server-wins).
62
+
63
+ When the server carries a remote provider (BYOK/managed set from the UI), the
64
+ provider/model/credential come from the server and any local ``base_url`` is
65
+ dropped. Otherwise (heuristic / terminal-set local_model, where the server has
66
+ no provider) the local provider/model/base_url are preserved and only the
67
+ variant is reconciled.
68
+ """
69
+ provider = str(record.get("inference_provider") or "").strip()
70
+ model_id = str(record.get("model_id") or "").strip()
71
+ credential_ref = str(record.get("provider_credential_ref") or "").strip()
72
+
73
+ fields: dict = {"execution_variant": server_variant}
74
+ if provider:
75
+ fields["provider"] = provider
76
+ if model_id:
77
+ fields["model_id"] = model_id
78
+ if credential_ref:
79
+ fields["credential_ref"] = credential_ref
80
+ else:
81
+ for key in ("provider", "model_id", "base_url", "credential_ref"):
82
+ if current.get(key):
83
+ fields[key] = current[key]
84
+ return fields
85
+
86
+
87
+ def sync_execution_variant_from_server(
88
+ project_root: Path,
89
+ *,
90
+ client: PlatformClient | None = None,
91
+ emit: Callable[[str], None] | None = None,
92
+ ) -> str | None:
93
+ """Pull the server project's execution settings into the managed config block.
94
+
95
+ Server-wins for the managed keys (execution_variant / provider / model_id /
96
+ credential_ref). Secrets (``provider_key``) are never written -- they stay in
97
+ the environment. Returns the applied variant when the local config changed,
98
+ else ``None`` (already in sync, offline, unauthenticated, or unbound project).
99
+ """
100
+ try:
101
+ project_id = resolve_project_id(project_root)
102
+ if not project_id:
103
+ return None
104
+ resolved = resolve_developer_key(project_root)
105
+ if not resolved:
106
+ return None
107
+ client = client or PlatformClient()
108
+ record = client.get_project_execution_mode(resolved.key, project_id)
109
+ except Exception:
110
+ return None
111
+
112
+ server_variant = normalize_execution_variant((record or {}).get("execution_mode"))
113
+ if not server_variant:
114
+ return None
115
+
116
+ config_path = _local_config_path(project_root)
117
+ if config_path is None:
118
+ return None
119
+
120
+ current = _current_managed_values(project_root)
121
+ desired = _desired_managed_fields(record or {}, current, server_variant)
122
+
123
+ # No-op when the managed keys already match (compare only managed keys).
124
+ current_managed = {k: current.get(k) for k in MANAGED_KEYS if current.get(k)}
125
+ if desired == current_managed:
126
+ return None
127
+
128
+ try:
129
+ text = config_path.read_text(encoding="utf-8")
130
+ new_text = upsert_managed_block(text, desired)
131
+ if not new_text.endswith("\n"):
132
+ new_text += "\n"
133
+ if new_text == text:
134
+ return None
135
+ config_path.write_text(new_text, encoding="utf-8")
136
+ except OSError:
137
+ return None
138
+
139
+ if emit:
140
+ emit(f"Synced execution_variant from server: {server_variant}")
141
+ return server_variant
142
+
143
+
144
+ def push_execution_variant_to_server(
145
+ project_root: Path,
146
+ variant: str,
147
+ *,
148
+ client: PlatformClient | None = None,
149
+ emit: Callable[[str], None] | None = None,
150
+ ) -> bool:
151
+ """Push a locally chosen execution variant to the server project.
152
+
153
+ Best-effort; returns ``True`` on a successful push, else ``False``.
154
+ """
155
+ normalized = normalize_execution_variant(variant)
156
+ if not normalized:
157
+ return False
158
+ try:
159
+ project_id = resolve_project_id(project_root)
160
+ if not project_id:
161
+ return False
162
+ resolved = resolve_developer_key(project_root)
163
+ if not resolved:
164
+ return False
165
+ client = client or PlatformClient()
166
+ client.set_project_execution_mode(resolved.key, project_id, normalized)
167
+ except Exception:
168
+ return False
169
+ if emit:
170
+ emit(f"Pushed execution_variant to server: {normalized}")
171
+ return True
@@ -598,38 +598,30 @@ def _local_model_provider_values() -> tuple[str, str]:
598
598
  return model_id, f"http://127.0.0.1:{port}/v1"
599
599
 
600
600
 
601
- def _apply_local_model_provider_block(text: str) -> str:
602
- """Swap the default heuristic provider block for an Ollama block (local_model).
603
-
604
- Only rewrites when the untouched heuristic default is still present, so a
605
- developer who already configured a provider is never overwritten.
606
- """
601
+ def _local_model_managed_fields() -> dict:
602
+ """Managed-block fields for ``local_model`` (Ollama provider + model/base_url)."""
607
603
  model_id, base_url = _local_model_provider_values()
608
- return text.replace(
609
- "# Heuristic (local, no LLM) - default for execution_variant: local_heuristic\n"
610
- "provider: heuristic\n"
611
- "model_id: heuristic-local-v1",
612
- "# Ollama (local engine) - configured by `refactor setup --mode local_model`\n"
613
- "provider: ollama\n"
614
- f"model_id: {model_id}\n"
615
- f"base_url: {base_url}",
616
- 1,
617
- )
604
+ return {
605
+ "execution_variant": LOCAL_MODEL,
606
+ "provider": "ollama",
607
+ "model_id": model_id,
608
+ "base_url": base_url,
609
+ }
618
610
 
619
611
 
620
612
  def _persist_variant_to_project_config(variant: str, emit: Callable[[str], None] | None = None) -> None:
621
- """Write the chosen ``execution_variant`` into the project's refactor.config.
613
+ """Write the chosen ``execution_variant`` into the CLI-managed config block.
622
614
 
623
615
  Keeps a UI-selected or terminal-selected mode as the single source of truth
624
616
  for subsequent runs (doctor/review/code read it from config). For
625
- ``local_model`` it also swaps the default heuristic provider block for a local
626
- Ollama block (docs/46: local_model provider config is a setup concern, not an
627
- init concern). Best-effort: a missing config or write error never fails setup.
617
+ ``local_model`` the block also carries the local Ollama provider/model/base_url
618
+ (docs/46: local_model provider config is a setup concern, not an init concern).
619
+ Best-effort: a missing config or write error never fails setup.
628
620
  """
629
621
  if not variant:
630
622
  return
631
623
  try:
632
- from refactor_core.constitution import find_project_files
624
+ from refactor_core.constitution import find_project_files, upsert_managed_block
633
625
 
634
626
  files = find_project_files()
635
627
  if not files:
@@ -637,20 +629,12 @@ def _persist_variant_to_project_config(variant: str, emit: Callable[[str], None]
637
629
  _consti_path, config_path = files
638
630
  config_path = Path(config_path)
639
631
  text = config_path.read_text(encoding="utf-8")
640
- if variant == LOCAL_MODEL:
641
- text = _apply_local_model_provider_block(text)
642
- lines = text.splitlines()
643
- new_line = f"execution_variant: {variant}"
644
- replaced = False
645
- for idx, line in enumerate(lines):
646
- if line.strip().startswith("execution_variant:"):
647
- if line.strip() != new_line:
648
- lines[idx] = new_line
649
- replaced = True
650
- break
651
- if not replaced:
652
- lines.append(new_line)
653
- config_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
632
+ fields = _local_model_managed_fields() if variant == LOCAL_MODEL else {"execution_variant": variant}
633
+ new_text = upsert_managed_block(text, fields)
634
+ if not new_text.endswith("\n"):
635
+ new_text += "\n"
636
+ if new_text != text:
637
+ config_path.write_text(new_text, encoding="utf-8")
654
638
  if emit:
655
639
  emit(f"Recorded execution_variant: {variant} in refactor.config.")
656
640
  except Exception:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.7.12
3
+ Version: 0.7.14
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.22
11
+ Requires-Dist: refactorai-core>=3.2.23
12
12
 
13
13
  # refactorai-cli
14
14
 
@@ -66,6 +66,55 @@ the managed host toolchain when no container runtime is available. `refactor
66
66
  doctor` reports the selected environment (and `refactor doctor --sandbox` shows
67
67
  its full state), so a run and diagnostics always agree.
68
68
 
69
+ ## Customizing refactor.config
70
+
71
+ `refactor init` writes a **bare-minimum** `refactor.config`: just `version`,
72
+ `project_id`, and a CLI-managed block seeded to `local_heuristic`. A normal
73
+ developer never edits this file by hand — pick the mode (and provider keys, if
74
+ any) in the web UI or via `refactor setup --mode <variant>`.
75
+
76
+ ### The managed block (single source of truth for the mode)
77
+
78
+ ```
79
+ # >>> refactor managed (synced from server; do not edit) >>>
80
+ execution_variant: cloud_byok
81
+ provider: openai
82
+ model_id: gpt-4.1-mini
83
+ credential_ref: cred_01J...
84
+ # <<< refactor managed <<<
85
+ ```
86
+
87
+ - Owned by the CLI. `refactor setup` (terminal) writes it immediately; a change
88
+ made in the web UI is pulled into this block on the next `refactor doctor` /
89
+ `refactor watch` / `refactor setup` (**server-wins**).
90
+ - Only these keys live here: `execution_variant`, `provider`, `model_id`,
91
+ `base_url` (local_model), `credential_ref`. **Secrets are never written** —
92
+ keep provider keys in the environment and reference them as `${ENV_VAR}`.
93
+
94
+ ### What each mode needs
95
+
96
+ | variant | you provide |
97
+ | ---------------- | ------------------------------------------------------- |
98
+ | `local_heuristic`| nothing |
99
+ | `local_model` | nothing (setup writes the Ollama provider/model block) |
100
+ | `local_byok` | provider API key in the env (`${VAR}`); provider/model via UI/setup |
101
+ | `local_managed` | nothing |
102
+ | `cloud_byok` | provider API key in env (or a UI credential); provider/model via UI/setup |
103
+ | `cloud_managed` | nothing |
104
+
105
+ ### Advanced knobs and precedence
106
+
107
+ Advanced settings (`sandbox`, `verification`, `intelligence`, `compliance`,
108
+ `watch`, `timeout`, `exclude`, `model_context_overrides`, …) are **not** written
109
+ by `init`; add only the ones you need anywhere **outside** the managed block.
110
+ `refactor init --full` writes the fully-annotated template with every knob if you
111
+ prefer to start from that.
112
+
113
+ Resolution precedence: CLI flags → environment (`provider_key`,
114
+ `REFACTOR_PROVIDER`) → the synced managed block → code defaults. A key you place
115
+ outside the managed block (later in the file) also takes precedence over the
116
+ block, so power users can pin values while normal syncing continues.
117
+
69
118
  ## Local development install
70
119
 
71
120
  From repository root:
@@ -2,4 +2,4 @@ typer>=0.12.0
2
2
  httpx>=0.27.0
3
3
  rich>=13.7.0
4
4
  PyYAML>=6.0.1
5
- refactorai-core>=3.2.22
5
+ refactorai-core>=3.2.23
@@ -1,140 +0,0 @@
1
- """Execution-mode sync between the server project and local ``refactor.config``.
2
-
3
- The web UI sets a project's ``execution_mode`` on the server (JWT). The CLI
4
- authenticates with a developer key and treats the server as the source of truth
5
- (server-wins): ``setup`` / ``doctor`` / ``watch`` pull the server mode and write
6
- it into the local ``refactor.config`` so a UI change reflects in terminal runs.
7
- ``refactor setup --mode`` also pushes the chosen variant up so both agree.
8
-
9
- All operations are best-effort: offline / unauthenticated / unbound projects are
10
- a silent no-op so they never break the surrounding command.
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- from pathlib import Path
16
- from typing import Callable
17
-
18
- from refactor_core.constitution import CONFIG_FILENAME, find_project_files
19
- from refactor_core.execution_variant import (
20
- normalize_execution_variant,
21
- resolve_execution_variant,
22
- )
23
- from refactor_core.store import resolve_project_id
24
-
25
- from refactorai_cli.client import PlatformClient
26
- from refactorai_cli.credentials import resolve_developer_key
27
-
28
-
29
- def _local_config_path(project_root: Path) -> Path | None:
30
- files = find_project_files(project_root)
31
- if files:
32
- return Path(files[1])
33
- candidate = Path(project_root) / CONFIG_FILENAME
34
- return candidate if candidate.is_file() else None
35
-
36
-
37
- def _read_local_variant(project_root: Path) -> str:
38
- files = find_project_files(project_root)
39
- if not files:
40
- return ""
41
- try:
42
- from refactor_core.constitution import load_constitution
43
-
44
- constitution = load_constitution(files[0])
45
- return resolve_execution_variant(getattr(constitution, "settings", {}) or {})
46
- except Exception:
47
- return ""
48
-
49
-
50
- def _write_variant_to_config(config_path: Path, variant: str) -> bool:
51
- """Replace (or append) the ``execution_variant`` line in refactor.config."""
52
- try:
53
- text = config_path.read_text(encoding="utf-8")
54
- except OSError:
55
- return False
56
- lines = text.splitlines()
57
- new_line = f"execution_variant: {variant}"
58
- replaced = False
59
- for idx, line in enumerate(lines):
60
- if line.strip().startswith("execution_variant:"):
61
- if line.strip() == new_line:
62
- return False
63
- lines[idx] = new_line
64
- replaced = True
65
- break
66
- if not replaced:
67
- lines.append(new_line)
68
- try:
69
- config_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
70
- except OSError:
71
- return False
72
- return True
73
-
74
-
75
- def sync_execution_variant_from_server(
76
- project_root: Path,
77
- *,
78
- client: PlatformClient | None = None,
79
- emit: Callable[[str], None] | None = None,
80
- ) -> str | None:
81
- """Pull the server project's execution mode into local config (server-wins).
82
-
83
- Returns the newly applied variant when the local config was updated, else
84
- ``None`` (already in sync, offline, unauthenticated, or unbound project).
85
- """
86
- try:
87
- project_id = resolve_project_id(project_root)
88
- if not project_id:
89
- return None
90
- resolved = resolve_developer_key(project_root)
91
- if not resolved:
92
- return None
93
- client = client or PlatformClient()
94
- record = client.get_project_execution_mode(resolved.key, project_id)
95
- except Exception:
96
- return None
97
-
98
- server_variant = normalize_execution_variant((record or {}).get("execution_mode"))
99
- if not server_variant:
100
- return None
101
- if server_variant == _read_local_variant(project_root):
102
- return None
103
- config_path = _local_config_path(project_root)
104
- if config_path is None:
105
- return None
106
- if _write_variant_to_config(config_path, server_variant):
107
- if emit:
108
- emit(f"Synced execution_variant from server: {server_variant}")
109
- return server_variant
110
- return None
111
-
112
-
113
- def push_execution_variant_to_server(
114
- project_root: Path,
115
- variant: str,
116
- *,
117
- client: PlatformClient | None = None,
118
- emit: Callable[[str], None] | None = None,
119
- ) -> bool:
120
- """Push a locally chosen execution variant to the server project.
121
-
122
- Best-effort; returns ``True`` on a successful push, else ``False``.
123
- """
124
- normalized = normalize_execution_variant(variant)
125
- if not normalized:
126
- return False
127
- try:
128
- project_id = resolve_project_id(project_root)
129
- if not project_id:
130
- return False
131
- resolved = resolve_developer_key(project_root)
132
- if not resolved:
133
- return False
134
- client = client or PlatformClient()
135
- client.set_project_execution_mode(resolved.key, project_id, normalized)
136
- except Exception:
137
- return False
138
- if emit:
139
- emit(f"Pushed execution_variant to server: {normalized}")
140
- return True