refactorai-cli 0.7.1__tar.gz → 0.7.3__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 (53) hide show
  1. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/PKG-INFO +2 -2
  2. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/pyproject.toml +2 -2
  3. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/__init__.py +1 -1
  4. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/client.py +25 -0
  5. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/auth_cmds.py +39 -2
  6. refactorai_cli-0.7.3/refactorai_cli/commands/policy_cmds.py +168 -0
  7. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/run_cmds.py +180 -13
  8. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/main.py +5 -0
  9. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli.egg-info/PKG-INFO +2 -2
  10. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli.egg-info/SOURCES.txt +1 -0
  11. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli.egg-info/requires.txt +1 -1
  12. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/README.md +0 -0
  13. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/auth.py +0 -0
  14. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/cloud_rr.py +0 -0
  15. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/__init__.py +0 -0
  16. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/account_cmds.py +0 -0
  17. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/branch_cmds.py +0 -0
  18. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/cloud_cmds.py +0 -0
  19. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/engine_cmds.py +0 -0
  20. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/hook_cmds.py +0 -0
  21. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/model_cmds.py +0 -0
  22. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/pre_push_cmds.py +0 -0
  23. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/request_cmds.py +0 -0
  24. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/rules_cmds.py +0 -0
  25. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/runtime_cmds.py +0 -0
  26. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
  27. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/setup_cmds.py +0 -0
  28. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/toolchains_cmds.py +0 -0
  29. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/watch_cmds.py +0 -0
  30. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commands/workspace_cmds.py +0 -0
  31. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commit_queue.py +0 -0
  32. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/commit_telemetry.py +0 -0
  33. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/control_plane.py +0 -0
  34. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/credentials.py +0 -0
  35. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/dotenv_loader.py +0 -0
  36. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/git_scope.py +0 -0
  37. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/local_constitution.py +0 -0
  38. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/local_engine_runtime.py +0 -0
  39. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/local_paths.py +0 -0
  40. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/model_policy.py +0 -0
  41. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/pre_push_gate.py +0 -0
  42. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/refactor_branch_store.py +0 -0
  43. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/review_runner.py +0 -0
  44. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/runtime_manager.py +0 -0
  45. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/settings.py +0 -0
  46. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/setup_flow.py +0 -0
  47. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/watch_ledger.py +0 -0
  48. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/watch_state.py +0 -0
  49. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli/watch_supervisor.py +0 -0
  50. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli.egg-info/dependency_links.txt +0 -0
  51. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli.egg-info/entry_points.txt +0 -0
  52. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/refactorai_cli.egg-info/top_level.txt +0 -0
  53. {refactorai_cli-0.7.1 → refactorai_cli-0.7.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.7.1
3
+ Version: 0.7.3
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.13
11
+ Requires-Dist: refactorai-core>=3.2.15
12
12
 
13
13
  # refactorai-cli
14
14
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "refactorai-cli"
3
- version = "0.7.1"
3
+ version = "0.7.3"
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.13",
15
+ "refactorai-core>=3.2.15",
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.1"
8
+ __version__ = "0.7.3"
@@ -118,3 +118,28 @@ class PlatformClient:
118
118
  )
119
119
  payload = response.json()
120
120
  return payload if isinstance(payload, list) else []
121
+
122
+ def resolve_org_bootstrap(self, developer_key: str) -> dict:
123
+ """Resolve the org governance bootstrap context for the key's account.
124
+
125
+ Returns ``{account_context, org_policy_profile, org_config_profile}``.
126
+ Org fields are ``None`` for personal accounts. Used by
127
+ ``refactor policy sync`` to refresh the local read-only org cache.
128
+ """
129
+ try:
130
+ response = httpx.get(
131
+ f"{self.base_url}/v1/keys/org-bootstrap",
132
+ headers={"Authorization": f"Bearer {developer_key}"},
133
+ timeout=self.timeout,
134
+ )
135
+ except httpx.HTTPError as exc:
136
+ raise PlatformError(f"Could not reach platform at {self.base_url}: {exc}") from exc
137
+ if response.status_code == 401:
138
+ raise PlatformError("Developer key is invalid or revoked", status_code=401)
139
+ if response.status_code >= 400:
140
+ raise PlatformError(
141
+ f"Org policy resolution failed ({response.status_code})",
142
+ status_code=response.status_code,
143
+ )
144
+ payload = response.json()
145
+ return payload if isinstance(payload, dict) else {}
@@ -1,14 +1,17 @@
1
- """`refactor login` and `refactor whoami`."""
1
+ """`refactor login`, `refactor logout`, and `refactor whoami`."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import shutil
6
+
5
7
  import typer
6
8
  from rich.console import Console
7
9
 
8
10
  from refactorai_cli.auth import AuthError, ensure_authenticated
9
11
  from refactorai_cli.client import PlatformClient, PlatformError
10
12
  from refactorai_cli.credentials import load_credentials, save_credentials
11
- from refactorai_cli.settings import mask_key, platform_url
13
+ from refactorai_cli.local_paths import credentials_path, validation_cache_dir
14
+ from refactorai_cli.settings import env_api_key, mask_key, platform_url
12
15
 
13
16
  console = Console()
14
17
 
@@ -86,3 +89,37 @@ def whoami() -> None:
86
89
  f"[bold]key[/bold]: {mask_key(ctx.key.key)} (source: {source})\n"
87
90
  f"[bold]quota_remaining[/bold]: {quota}{suffix}"
88
91
  )
92
+
93
+
94
+ def logout() -> None:
95
+ """Clear central credentials and cached key validation state."""
96
+ creds_path = credentials_path()
97
+ cache_dir = validation_cache_dir()
98
+ removed_credentials = False
99
+ removed_cache = False
100
+
101
+ try:
102
+ if creds_path.exists():
103
+ creds_path.unlink()
104
+ removed_credentials = True
105
+ except OSError as exc:
106
+ console.print(f"[red]Logout failed:[/red] Could not remove {creds_path}: {exc}")
107
+ raise typer.Exit(code=1) from exc
108
+
109
+ try:
110
+ if cache_dir.exists():
111
+ shutil.rmtree(cache_dir)
112
+ removed_cache = True
113
+ except OSError as exc:
114
+ console.print(f"[red]Logout failed:[/red] Could not remove {cache_dir}: {exc}")
115
+ raise typer.Exit(code=1) from exc
116
+
117
+ if removed_credentials or removed_cache:
118
+ console.print("[green]Logged out.[/green] Cleared saved credentials and validation cache.")
119
+ else:
120
+ console.print("[dim]No saved credentials were found.[/dim]")
121
+ if env_api_key():
122
+ console.print(
123
+ "[yellow]Note:[/yellow] REFACTOR_API_KEY is set in your environment; "
124
+ "that key still takes precedence."
125
+ )
@@ -0,0 +1,168 @@
1
+ """``refactor policy`` — inspect and sync organization governance.
2
+
3
+ Organization policy is authoritative on the cloud and cached read-only under
4
+ ``~/.refactor`` (never in the repo). These commands let a developer:
5
+
6
+ * ``refactor policy show`` — view the resolved org policy + config guardrails
7
+ and their integrity/version status (offline, read-only).
8
+ * ``refactor policy sync`` — refresh the local cache from the cloud (resolves
9
+ ``ORG_POLICY_STALE`` after an admin publishes a new version, or restores a
10
+ locally tampered cache).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+
17
+ import typer
18
+ from rich.console import Console
19
+
20
+ from refactor_core.org_policy import verify_policy_integrity
21
+ from refactor_core.store import (
22
+ read_governance_events,
23
+ read_org_policy_cache,
24
+ resolve_project_id as resolve_store_project_id,
25
+ summarize_governance_events,
26
+ write_org_policy_cache,
27
+ )
28
+
29
+ from refactorai_cli.auth import AuthError, ensure_authenticated
30
+ from refactorai_cli.client import PlatformClient, PlatformError
31
+
32
+ app = typer.Typer(help="Inspect and sync organization governance policy.")
33
+ console = Console()
34
+
35
+
36
+ def _project_root() -> Path:
37
+ try:
38
+ return Path.cwd()
39
+ except OSError as exc:
40
+ console.print("[red]Could not resolve the current project directory.[/red]")
41
+ raise typer.Exit(code=1) from exc
42
+
43
+
44
+ @app.command("show")
45
+ def show() -> None:
46
+ """Show the resolved organization policy and config guardrails (read-only)."""
47
+ project_root = _project_root()
48
+ pid = resolve_store_project_id(project_root)
49
+ cached = read_org_policy_cache(project_root, project_id=pid or None)
50
+ if not cached:
51
+ console.print(
52
+ "[dim]This project is not organization-governed "
53
+ "(personal account, or no org policy resolved at init).[/dim]"
54
+ )
55
+ raise typer.Exit(code=0)
56
+
57
+ markdown, meta = cached
58
+ policy = meta.get("policy", {}) if isinstance(meta, dict) else {}
59
+ config = meta.get("config", {}) if isinstance(meta, dict) else {}
60
+ account_slug = meta.get("account_slug") if isinstance(meta, dict) else ""
61
+
62
+ source = str(policy.get("source") or "system_default")
63
+ version = policy.get("version")
64
+ integrity_ok = verify_policy_integrity(markdown, str(policy.get("hash") or ""))
65
+
66
+ console.print(f"[bold]Organization[/bold]: {account_slug or '?'}")
67
+ console.print(f"[bold]Policy source[/bold]: {source} (v{version})")
68
+ console.print(
69
+ f"[bold]Integrity[/bold]: "
70
+ + ("[green]OK[/green]" if integrity_ok else "[red]TAMPERED — run `refactor policy sync`[/red]")
71
+ )
72
+ console.print(f"[bold]Resolved at[/bold]: {meta.get('resolved_at') if isinstance(meta, dict) else '?'}")
73
+
74
+ locked = list(config.get("locked_paths") or [])
75
+ required = list(config.get("required_paths") or [])
76
+ forbidden = list(config.get("forbidden_paths") or [])
77
+ console.print(
78
+ "[bold]Config guardrails[/bold]: "
79
+ f"locked={locked or '—'} required={required or '—'} forbidden={forbidden or '—'}"
80
+ )
81
+
82
+ summary = summarize_governance_events(
83
+ read_governance_events(project_root, project_id=pid or None)
84
+ )
85
+ if summary.get("total"):
86
+ counts = summary.get("counts", {})
87
+ console.print(
88
+ "[bold]Enforcement history[/bold]: "
89
+ f"ok={counts.get('ok', 0)} "
90
+ f"tamper={counts.get('tamper', 0)} "
91
+ f"guardrail_violation={counts.get('guardrail_violation', 0)} "
92
+ f"(last {summary.get('last_ts') or '—'})"
93
+ )
94
+
95
+ console.print("\n[bold]Organization Policy[/bold]:")
96
+ if markdown.strip():
97
+ console.print(markdown.strip())
98
+ else:
99
+ console.print("[dim](none — organization has not published a policy; system defaults apply)[/dim]")
100
+
101
+
102
+ @app.command("sync")
103
+ def sync() -> None:
104
+ """Refresh the local org policy/config cache from the cloud."""
105
+ project_root = _project_root()
106
+ pid = resolve_store_project_id(project_root)
107
+
108
+ try:
109
+ auth_ctx = ensure_authenticated(project_root, force_remote=True)
110
+ except AuthError as exc:
111
+ console.print(f"[red]Authentication required to sync org policy:[/red] {exc}")
112
+ raise typer.Exit(code=1) from exc
113
+
114
+ client = PlatformClient()
115
+ try:
116
+ data = client.resolve_org_bootstrap(auth_ctx.key.key)
117
+ except PlatformError as exc:
118
+ console.print(f"[red]Could not resolve organization policy:[/red] {exc}")
119
+ raise typer.Exit(code=1) from exc
120
+
121
+ account_ctx = data.get("account_context") or {}
122
+ policy_ctx = data.get("org_policy_profile")
123
+ config_ctx = data.get("org_config_profile")
124
+
125
+ if not policy_ctx and not config_ctx:
126
+ console.print(
127
+ "[dim]Account is personal / not organization-governed; nothing to sync.[/dim]"
128
+ )
129
+ raise typer.Exit(code=0)
130
+
131
+ policy_ctx = policy_ctx or {}
132
+ config_ctx = config_ctx or {}
133
+
134
+ previous = read_org_policy_cache(project_root, project_id=pid or None)
135
+ old_version = None
136
+ if previous is not None:
137
+ old_version = (previous[1].get("policy", {}) or {}).get("version")
138
+
139
+ write_org_policy_cache(
140
+ project_root,
141
+ project_id=pid or None,
142
+ account_slug=str(account_ctx.get("slug") or auth_ctx.account_slug or ""),
143
+ policy_source=str(policy_ctx.get("source") or "system_default"),
144
+ policy_version=int(policy_ctx.get("version") or 0),
145
+ policy_hash=str(policy_ctx.get("org_policies_hash") or ""),
146
+ policy_markdown=str(policy_ctx.get("org_policies_markdown") or ""),
147
+ policy_profile_id=policy_ctx.get("profile_id"),
148
+ config_source=str(config_ctx.get("source") or "system_default"),
149
+ config_version=int(config_ctx.get("version") or 0),
150
+ config_profile_id=config_ctx.get("profile_id"),
151
+ config_template_yaml=str(config_ctx.get("config_template_yaml") or ""),
152
+ locked_paths=list(config_ctx.get("locked_paths") or []),
153
+ required_paths=list(config_ctx.get("required_paths") or []),
154
+ forbidden_paths=list(config_ctx.get("forbidden_paths") or []),
155
+ )
156
+
157
+ new_version = int(policy_ctx.get("version") or 0)
158
+ slug = account_ctx.get("slug") or auth_ctx.account_slug or "?"
159
+ if old_version is not None and new_version != old_version:
160
+ console.print(
161
+ f"[green]Synced[/green] organization policy for [bold]{slug}[/bold]: "
162
+ f"v{old_version} → v{new_version}."
163
+ )
164
+ else:
165
+ console.print(
166
+ f"[green]Synced[/green] organization policy for [bold]{slug}[/bold] (v{new_version})."
167
+ )
168
+ console.print("[dim]Cache refreshed under ~/.refactor (never written into your repo).[/dim]")
@@ -33,9 +33,16 @@ from refactor_core.constitution import (
33
33
  has_raw_secret_literal,
34
34
  load_config,
35
35
  load_constitution,
36
+ parse_config,
36
37
  raw_secret_matches,
37
38
  raw_secret_matches_config,
38
39
  )
40
+ from refactor_core.org_policy import (
41
+ ORG_POLICY_TAMPERED,
42
+ assemble_effective_body,
43
+ check_config_guardrails,
44
+ verify_policy_integrity,
45
+ )
39
46
  from refactor_core.compliance_settings import (
40
47
  ComplianceSelectionError,
41
48
  resolve_compliance_selection,
@@ -91,8 +98,11 @@ from refactor_core.store import (
91
98
  head_run_dir,
92
99
  list_runs,
93
100
  prune_runs,
101
+ read_org_policy_cache,
102
+ record_governance_event,
94
103
  resolve_project_id as resolve_store_project_id,
95
104
  resolve_store_dir,
105
+ write_org_policy_cache,
96
106
  read_findings,
97
107
  read_report,
98
108
  read_sandbox_install_history,
@@ -911,9 +921,115 @@ def _load_project() -> tuple[Path, object, object]:
911
921
  constitution = load_constitution(consti_path)
912
922
  config = load_config(config_path)
913
923
  _warn_raw_key(constitution, config)
924
+ _apply_org_governance(consti_path.parent, constitution, config)
914
925
  return consti_path.parent, constitution, config
915
926
 
916
927
 
928
+ def _apply_org_governance(project_root: Path, constitution, config) -> None:
929
+ """Enforce org governance and assemble the effective constitution.
930
+
931
+ For org-governed projects (a home-level org cache exists) this runs three
932
+ deterministic, offline checks before any command touches the pipeline:
933
+
934
+ 1. Tamper detection — the cached org policy must still match its pinned hash.
935
+ 2. Config guardrails — locked/required/forbidden config paths.
936
+ 3. Precedence assembly — org policy is prepended to the constitution body so
937
+ the engine always sees it first ("org wins").
938
+
939
+ Personal accounts (no cache) are a no-op. ``REFACTOR_ORG_POLICY_ENFORCEMENT
940
+ =warn`` downgrades blocking to warnings for emergency use.
941
+ """
942
+ try:
943
+ pid = resolve_store_project_id(project_root)
944
+ cached = read_org_policy_cache(project_root, project_id=pid or None)
945
+ except Exception:
946
+ cached = None
947
+ if not cached:
948
+ return
949
+
950
+ markdown, meta = cached
951
+ policy_meta = meta.get("policy", {}) if isinstance(meta, dict) else {}
952
+ config_meta = meta.get("config", {}) if isinstance(meta, dict) else {}
953
+ account_slug = str(meta.get("account_slug") or "") if isinstance(meta, dict) else ""
954
+ warn_only = (
955
+ str(os.environ.get("REFACTOR_ORG_POLICY_ENFORCEMENT", "block")).strip().lower() == "warn"
956
+ )
957
+
958
+ pid_for_ledger = None
959
+ try:
960
+ pid_for_ledger = resolve_store_project_id(project_root)
961
+ except Exception:
962
+ pid_for_ledger = None
963
+
964
+ def _telemetry(event: str, *, detail: str = "", codes: list[str] | None = None) -> None:
965
+ # Local-only observability; never let it break the command.
966
+ try:
967
+ record_governance_event(
968
+ project_root,
969
+ project_id=pid_for_ledger or None,
970
+ event=event,
971
+ detail=detail,
972
+ codes=codes,
973
+ )
974
+ except Exception:
975
+ pass
976
+
977
+ def _block(message: str) -> None:
978
+ if warn_only:
979
+ console.print(f"[yellow]Warning (enforcement bypassed):[/yellow] {message}")
980
+ return
981
+ console.print(message)
982
+ raise typer.Exit(code=1)
983
+
984
+ # 1) Tamper detection.
985
+ if not verify_policy_integrity(markdown, str(policy_meta.get("hash") or "")):
986
+ _telemetry("tamper", detail=account_slug, codes=[ORG_POLICY_TAMPERED])
987
+ _block(
988
+ f"[red]{ORG_POLICY_TAMPERED}[/red]: the cached organization policy for account "
989
+ f"[bold]{account_slug or '?'}[/bold] was modified locally and no longer matches the "
990
+ "version published by your organization. Run `refactor policy sync` to restore it."
991
+ )
992
+
993
+ # 2) Config guardrails.
994
+ org_settings: dict = {}
995
+ template_yaml = str(config_meta.get("template_yaml") or "")
996
+ if template_yaml.strip():
997
+ try:
998
+ org_settings = dict(parse_config(template_yaml).settings or {})
999
+ except Exception:
1000
+ org_settings = {}
1001
+ violations = check_config_guardrails(
1002
+ dict(getattr(config, "settings", {}) or {}),
1003
+ org_settings,
1004
+ locked_paths=list(config_meta.get("locked_paths") or []),
1005
+ required_paths=list(config_meta.get("required_paths") or []),
1006
+ forbidden_paths=list(config_meta.get("forbidden_paths") or []),
1007
+ )
1008
+ if violations:
1009
+ _telemetry(
1010
+ "guardrail_violation",
1011
+ detail=account_slug,
1012
+ codes=[v.code for v in violations],
1013
+ )
1014
+ for violation in violations:
1015
+ console.print(f"[red]{violation.code}[/red]: {violation.detail}")
1016
+ console.print(
1017
+ "[dim]These settings are governed by your organization. Update "
1018
+ f"{CONFIG_FILENAME} to comply, or ask an org admin to change the policy.[/dim]"
1019
+ )
1020
+ if not warn_only:
1021
+ raise typer.Exit(code=1)
1022
+
1023
+ # 3) Effective-constitution assembly (org policy first; charter underneath).
1024
+ if markdown.strip():
1025
+ constitution.body = assemble_effective_body(
1026
+ getattr(constitution, "body", "") or "", markdown
1027
+ )
1028
+
1029
+ # Clean pass: record a compliant outcome for observability.
1030
+ _telemetry("ok", detail=account_slug)
1031
+
1032
+
917
1033
  def _print_store_context(project_root) -> None:
918
1034
  """Surface the resolved project id + central store dir for this run.
919
1035
 
@@ -1150,16 +1266,54 @@ def init(
1150
1266
  config_target.write_text(config_text, encoding="utf-8")
1151
1267
 
1152
1268
  constitution = load_constitution(consti_target)
1153
- record = register_project(
1154
- project_root, constitution_hash=constitution.content_hash, project_id=project_id
1155
- )
1269
+ register_project(project_root, constitution_hash=constitution.content_hash, project_id=project_id)
1270
+
1271
+ # Org-governed projects: materialize the resolved org policy + config
1272
+ # guardrails at HOME level only (never in the repo). The committed
1273
+ # refactor.consti stays charter-only; the org policy is authoritative on the
1274
+ # cloud and cached read-only under ~/.refactor for offline/runtime assembly.
1275
+ org_policy_ctx = created.get("org_policy_profile") if isinstance(created, dict) else None
1276
+ org_config_ctx = created.get("org_config_profile") if isinstance(created, dict) else None
1277
+ account_ctx = created.get("account_context") if isinstance(created, dict) else None
1278
+ org_cached_meta: dict | None = None
1279
+ if isinstance(org_policy_ctx, dict) or isinstance(org_config_ctx, dict):
1280
+ policy_ctx = org_policy_ctx or {}
1281
+ config_ctx = org_config_ctx or {}
1282
+ org_cached_meta = write_org_policy_cache(
1283
+ project_root,
1284
+ project_id=project_id,
1285
+ account_slug=str((account_ctx or {}).get("slug") or account_slug or ""),
1286
+ policy_source=str(policy_ctx.get("source") or "system_default"),
1287
+ policy_version=int(policy_ctx.get("version") or 0),
1288
+ policy_hash=str(policy_ctx.get("org_policies_hash") or ""),
1289
+ policy_markdown=str(policy_ctx.get("org_policies_markdown") or ""),
1290
+ policy_profile_id=policy_ctx.get("profile_id"),
1291
+ config_source=str(config_ctx.get("source") or "system_default"),
1292
+ config_version=int(config_ctx.get("version") or 0),
1293
+ config_profile_id=config_ctx.get("profile_id"),
1294
+ config_template_yaml=str(config_ctx.get("config_template_yaml") or ""),
1295
+ locked_paths=list(config_ctx.get("locked_paths") or []),
1296
+ required_paths=list(config_ctx.get("required_paths") or []),
1297
+ forbidden_paths=list(config_ctx.get("forbidden_paths") or []),
1298
+ )
1299
+
1156
1300
  console.print(f"[green]Created[/green] {consti_target}")
1157
1301
  console.print(f"[green]Created[/green] {config_target}")
1158
- account_note = f" (account {account_slug})" if account_slug else ""
1302
+ account_type = str((account_ctx or {}).get("type") or "").strip().lower()
1303
+ account_name = str((account_ctx or {}).get("display_name") or "").strip()
1304
+ account_slug = str((account_ctx or {}).get("slug") or account_slug or "").strip()
1305
+ if account_type == "organization":
1306
+ account_label = "organization"
1307
+ elif account_type:
1308
+ account_label = account_type
1309
+ else:
1310
+ account_label = "account"
1311
+ account_parts = [part for part in (account_name, account_slug) if part]
1312
+ account_value = " / ".join(account_parts) if account_parts else auth_ctx.account_id
1159
1313
  console.print(
1160
- f"[green]Linked[/green] to cloud project [bold]{project_name}[/bold]"
1161
- f"{account_note}. The project id is saved in {CONFIG_FILENAME}; "
1162
- "rename the project or attach a repository anytime from the web UI."
1314
+ f"[green]Linked[/green] cloud project [bold]{project_name}[/bold]. "
1315
+ f"{account_label.title()}: [bold]{account_value}[/bold]. "
1316
+ f"Project id saved in {CONFIG_FILENAME}."
1163
1317
  )
1164
1318
  if config_profile == "local_server":
1165
1319
  console.print(
@@ -1172,13 +1326,26 @@ def init(
1172
1326
  "(mode=cloud_byok, env-key-first via provider_key; credential_ref is optional fallback)."
1173
1327
  )
1174
1328
  console.print(
1175
- f"[green]Registered[/green] project in central store: {resolve_store_dir(project_root)}"
1176
- )
1177
- console.print(
1178
- f"Only {CONSTITUTION_FILENAME} and {CONFIG_FILENAME} are added to your repo; "
1179
- "all run state stays under ~/.refactor "
1180
- f"(encoded: {record['encoded']})."
1329
+ f"Only {CONSTITUTION_FILENAME} and {CONFIG_FILENAME} are added to your repo. "
1330
+ "Runtime state stays outside the repository."
1181
1331
  )
1332
+ if org_cached_meta is not None:
1333
+ policy_meta = org_cached_meta.get("policy", {}) if isinstance(org_cached_meta, dict) else {}
1334
+ source = str(policy_meta.get("source") or "system_default")
1335
+ version = policy_meta.get("version")
1336
+ if source == "active":
1337
+ console.print(
1338
+ f"[green]Applied[/green] organization policy (v{version}) from account "
1339
+ f"[bold]{org_cached_meta.get('account_slug') or account_slug}[/bold]. "
1340
+ f"It is cached locally and layered above your "
1341
+ f"editable {CONSTITUTION_FILENAME} charter at run time; it is never "
1342
+ "written into your repo."
1343
+ )
1344
+ else:
1345
+ console.print(
1346
+ "[dim]Organization has no published policy yet; using system defaults. "
1347
+ f"Your {CONSTITUTION_FILENAME} charter is fully in effect.[/dim]"
1348
+ )
1182
1349
  _offer_commit_hook(project_root)
1183
1350
 
1184
1351
 
@@ -16,6 +16,7 @@ from refactorai_cli.commands import (
16
16
  engine_cmds,
17
17
  hook_cmds,
18
18
  model_cmds,
19
+ policy_cmds,
19
20
  pre_push_cmds,
20
21
  request_cmds,
21
22
  runtime_cmds,
@@ -118,6 +119,7 @@ def main(
118
119
 
119
120
  # Auth (R7.1)
120
121
  app.command()(auth_cmds.login)
122
+ app.command()(auth_cmds.logout)
121
123
  app.command()(auth_cmds.whoami)
122
124
  app.add_typer(account_cmds.app, name="account")
123
125
 
@@ -172,6 +174,9 @@ app.add_typer(watch_cmds.watch_app, name="watch")
172
174
  app.add_typer(branch_cmds.app, name="branch")
173
175
  app.add_typer(request_cmds.app, name="request")
174
176
 
177
+ # Organization governance: inspect/sync org policy + config guardrails (docs/45).
178
+ app.add_typer(policy_cmds.app, name="policy")
179
+
175
180
 
176
181
  if __name__ == "__main__":
177
182
  app()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.7.1
3
+ Version: 0.7.3
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.13
11
+ Requires-Dist: refactorai-core>=3.2.15
12
12
 
13
13
  # refactorai-cli
14
14
 
@@ -38,6 +38,7 @@ refactorai_cli/commands/cloud_cmds.py
38
38
  refactorai_cli/commands/engine_cmds.py
39
39
  refactorai_cli/commands/hook_cmds.py
40
40
  refactorai_cli/commands/model_cmds.py
41
+ refactorai_cli/commands/policy_cmds.py
41
42
  refactorai_cli/commands/pre_push_cmds.py
42
43
  refactorai_cli/commands/request_cmds.py
43
44
  refactorai_cli/commands/rules_cmds.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.2.13
5
+ refactorai-core>=3.2.15
File without changes
File without changes