refactorai-cli 0.7.0__tar.gz → 0.7.2__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.0 → refactorai_cli-0.7.2}/PKG-INFO +2 -2
  2. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/pyproject.toml +2 -2
  3. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/__init__.py +1 -1
  4. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/client.py +25 -0
  5. refactorai_cli-0.7.2/refactorai_cli/commands/policy_cmds.py +168 -0
  6. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/run_cmds.py +163 -0
  7. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/watch_cmds.py +26 -1
  8. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/main.py +4 -0
  9. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli.egg-info/PKG-INFO +2 -2
  10. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli.egg-info/SOURCES.txt +1 -0
  11. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli.egg-info/requires.txt +1 -1
  12. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/README.md +0 -0
  13. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/auth.py +0 -0
  14. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/cloud_rr.py +0 -0
  15. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/__init__.py +0 -0
  16. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/account_cmds.py +0 -0
  17. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/auth_cmds.py +0 -0
  18. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/branch_cmds.py +0 -0
  19. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/cloud_cmds.py +0 -0
  20. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/engine_cmds.py +0 -0
  21. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/hook_cmds.py +0 -0
  22. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/model_cmds.py +0 -0
  23. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/pre_push_cmds.py +0 -0
  24. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/request_cmds.py +0 -0
  25. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/rules_cmds.py +0 -0
  26. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/runtime_cmds.py +0 -0
  27. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
  28. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/setup_cmds.py +0 -0
  29. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/toolchains_cmds.py +0 -0
  30. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/workspace_cmds.py +0 -0
  31. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commit_queue.py +0 -0
  32. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commit_telemetry.py +0 -0
  33. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/control_plane.py +0 -0
  34. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/credentials.py +0 -0
  35. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/dotenv_loader.py +0 -0
  36. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/git_scope.py +0 -0
  37. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/local_constitution.py +0 -0
  38. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/local_engine_runtime.py +0 -0
  39. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/local_paths.py +0 -0
  40. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/model_policy.py +0 -0
  41. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/pre_push_gate.py +0 -0
  42. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/refactor_branch_store.py +0 -0
  43. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/review_runner.py +0 -0
  44. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/runtime_manager.py +0 -0
  45. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/settings.py +0 -0
  46. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/setup_flow.py +0 -0
  47. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/watch_ledger.py +0 -0
  48. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/watch_state.py +0 -0
  49. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/watch_supervisor.py +0 -0
  50. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli.egg-info/dependency_links.txt +0 -0
  51. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli.egg-info/entry_points.txt +0 -0
  52. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli.egg-info/top_level.txt +0 -0
  53. {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.7.0
3
+ Version: 0.7.2
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.0"
3
+ version = "0.7.2"
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.0"
8
+ __version__ = "0.7.2"
@@ -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 {}
@@ -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
 
@@ -1153,6 +1269,36 @@ def init(
1153
1269
  record = register_project(
1154
1270
  project_root, constitution_hash=constitution.content_hash, project_id=project_id
1155
1271
  )
1272
+
1273
+ # Org-governed projects: materialize the resolved org policy + config
1274
+ # guardrails at HOME level only (never in the repo). The committed
1275
+ # refactor.consti stays charter-only; the org policy is authoritative on the
1276
+ # cloud and cached read-only under ~/.refactor for offline/runtime assembly.
1277
+ org_policy_ctx = created.get("org_policy_profile") if isinstance(created, dict) else None
1278
+ org_config_ctx = created.get("org_config_profile") if isinstance(created, dict) else None
1279
+ account_ctx = created.get("account_context") if isinstance(created, dict) else None
1280
+ org_cached_meta: dict | None = None
1281
+ if isinstance(org_policy_ctx, dict) or isinstance(org_config_ctx, dict):
1282
+ policy_ctx = org_policy_ctx or {}
1283
+ config_ctx = org_config_ctx or {}
1284
+ org_cached_meta = write_org_policy_cache(
1285
+ project_root,
1286
+ project_id=project_id,
1287
+ account_slug=str((account_ctx or {}).get("slug") or account_slug or ""),
1288
+ policy_source=str(policy_ctx.get("source") or "system_default"),
1289
+ policy_version=int(policy_ctx.get("version") or 0),
1290
+ policy_hash=str(policy_ctx.get("org_policies_hash") or ""),
1291
+ policy_markdown=str(policy_ctx.get("org_policies_markdown") or ""),
1292
+ policy_profile_id=policy_ctx.get("profile_id"),
1293
+ config_source=str(config_ctx.get("source") or "system_default"),
1294
+ config_version=int(config_ctx.get("version") or 0),
1295
+ config_profile_id=config_ctx.get("profile_id"),
1296
+ config_template_yaml=str(config_ctx.get("config_template_yaml") or ""),
1297
+ locked_paths=list(config_ctx.get("locked_paths") or []),
1298
+ required_paths=list(config_ctx.get("required_paths") or []),
1299
+ forbidden_paths=list(config_ctx.get("forbidden_paths") or []),
1300
+ )
1301
+
1156
1302
  console.print(f"[green]Created[/green] {consti_target}")
1157
1303
  console.print(f"[green]Created[/green] {config_target}")
1158
1304
  account_note = f" (account {account_slug})" if account_slug else ""
@@ -1179,6 +1325,23 @@ def init(
1179
1325
  "all run state stays under ~/.refactor "
1180
1326
  f"(encoded: {record['encoded']})."
1181
1327
  )
1328
+ if org_cached_meta is not None:
1329
+ policy_meta = org_cached_meta.get("policy", {}) if isinstance(org_cached_meta, dict) else {}
1330
+ source = str(policy_meta.get("source") or "system_default")
1331
+ version = policy_meta.get("version")
1332
+ if source == "active":
1333
+ console.print(
1334
+ f"[green]Applied[/green] organization policy (v{version}) from account "
1335
+ f"[bold]{org_cached_meta.get('account_slug') or account_slug}[/bold]. "
1336
+ f"It is cached read-only under ~/.refactor and layered above your "
1337
+ f"editable {CONSTITUTION_FILENAME} charter at run time; it is never "
1338
+ "written into your repo."
1339
+ )
1340
+ else:
1341
+ console.print(
1342
+ "[dim]Organization has no published policy yet; using system defaults. "
1343
+ f"Your {CONSTITUTION_FILENAME} charter is fully in effect.[/dim]"
1344
+ )
1182
1345
  _offer_commit_hook(project_root)
1183
1346
 
1184
1347
 
@@ -909,7 +909,29 @@ def _run_tui(project_root: Path, state: WatchState, runner: ReviewRunner) -> Non
909
909
  _print_agent_intro("watch")
910
910
  from rich.live import Live
911
911
 
912
- live = Live(render_graph(state), console=console, auto_refresh=False, screen=False)
912
+ # Without a real TTY (piped, captured, CI, some multiplexer log panes) Rich
913
+ # cannot move the cursor to overwrite the previous frame, so an inline Live
914
+ # re-emits the whole graph on every refresh and the terminal fills with
915
+ # stacked copies. In that case skip Live entirely: process the queue once and
916
+ # print a single static frame.
917
+ if not console.is_terminal:
918
+ process_pending(project_root, state, runner)
919
+ state.select_default()
920
+ console.print(render_graph(state))
921
+ return
922
+
923
+ # ``screen=True`` renders into the alternate screen buffer, which is cleared
924
+ # and redrawn each frame, so frames can never stack regardless of graph
925
+ # height; ``vertical_overflow="crop"`` keeps an oversized graph within the
926
+ # viewport instead of spilling extra lines. Normal scrollback is restored on
927
+ # exit, so a final static frame is printed afterwards for history.
928
+ live = Live(
929
+ render_graph(state),
930
+ console=console,
931
+ auto_refresh=False,
932
+ screen=True,
933
+ vertical_overflow="crop",
934
+ )
913
935
  live.start()
914
936
  refresh_lock = threading.Lock()
915
937
 
@@ -943,6 +965,9 @@ def _run_tui(project_root: Path, state: WatchState, runner: ReviewRunner) -> Non
943
965
  refresh(state)
944
966
  finally:
945
967
  live.stop()
968
+ # ``screen=True`` tears down the alternate buffer on stop, taking the
969
+ # graph with it; reprint the last frame so it stays in scrollback.
970
+ console.print(render_graph(state))
946
971
 
947
972
 
948
973
  watch_app = typer.Typer(
@@ -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,
@@ -172,6 +173,9 @@ app.add_typer(watch_cmds.watch_app, name="watch")
172
173
  app.add_typer(branch_cmds.app, name="branch")
173
174
  app.add_typer(request_cmds.app, name="request")
174
175
 
176
+ # Organization governance: inspect/sync org policy + config guardrails (docs/45).
177
+ app.add_typer(policy_cmds.app, name="policy")
178
+
175
179
 
176
180
  if __name__ == "__main__":
177
181
  app()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.7.0
3
+ Version: 0.7.2
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