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.
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/PKG-INFO +2 -2
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/pyproject.toml +2 -2
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/__init__.py +1 -1
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/client.py +25 -0
- refactorai_cli-0.7.2/refactorai_cli/commands/policy_cmds.py +168 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/run_cmds.py +163 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/watch_cmds.py +26 -1
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/main.py +4 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli.egg-info/PKG-INFO +2 -2
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli.egg-info/SOURCES.txt +1 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli.egg-info/requires.txt +1 -1
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/README.md +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/auth.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/cloud_rr.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/__init__.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/account_cmds.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/auth_cmds.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/branch_cmds.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/cloud_cmds.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/engine_cmds.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/hook_cmds.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/model_cmds.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/pre_push_cmds.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/request_cmds.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/rules_cmds.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/runtime_cmds.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/setup_cmds.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/toolchains_cmds.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commands/workspace_cmds.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commit_queue.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/commit_telemetry.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/control_plane.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/credentials.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/dotenv_loader.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/git_scope.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/local_constitution.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/local_engine_runtime.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/local_paths.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/model_policy.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/pre_push_gate.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/refactor_branch_store.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/review_runner.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/runtime_manager.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/settings.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/setup_flow.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/watch_ledger.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/watch_state.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli/watch_supervisor.py +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli.egg-info/dependency_links.txt +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli.egg-info/entry_points.txt +0 -0
- {refactorai_cli-0.7.0 → refactorai_cli-0.7.2}/refactorai_cli.egg-info/top_level.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
15
|
+
"refactorai-core>=3.2.15",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
[project.scripts]
|
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|