refactorai-cli 0.3.4__tar.gz → 0.3.5__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.3.4 → refactorai_cli-0.3.5}/PKG-INFO +1 -1
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/pyproject.toml +1 -1
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/__init__.py +1 -1
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/client.py +18 -0
- refactorai_cli-0.3.5/refactorai_cli/commands/account_cmds.py +76 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/commands/auth_cmds.py +3 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/commands/run_cmds.py +254 -6
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/commands/runtime_proxy_cmds.py +7 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/main.py +47 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli.egg-info/PKG-INFO +1 -1
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli.egg-info/SOURCES.txt +1 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/README.md +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/auth.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/cloud_rr.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/commands/__init__.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/commands/cloud_cmds.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/commands/engine_cmds.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/commands/model_cmds.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/commands/rules_cmds.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/commands/runtime_cmds.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/commands/setup_cmds.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/control_plane.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/credentials.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/local_constitution.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/local_engine_runtime.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/local_paths.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/model_policy.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/runtime_manager.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/settings.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli/setup_flow.py +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli.egg-info/dependency_links.txt +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli.egg-info/entry_points.txt +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli.egg-info/requires.txt +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/refactorai_cli.egg-info/top_level.txt +0 -0
- {refactorai_cli-0.3.4 → refactorai_cli-0.3.5}/setup.cfg +0 -0
|
@@ -44,3 +44,21 @@ class PlatformClient:
|
|
|
44
44
|
f"Key validation failed ({response.status_code})", status_code=response.status_code
|
|
45
45
|
)
|
|
46
46
|
return response.json()
|
|
47
|
+
|
|
48
|
+
def list_key_accounts(self, developer_key: str) -> list[dict]:
|
|
49
|
+
try:
|
|
50
|
+
response = httpx.get(
|
|
51
|
+
f"{self.base_url}/v1/keys/accounts",
|
|
52
|
+
headers={"Authorization": f"Bearer {developer_key}"},
|
|
53
|
+
timeout=self.timeout,
|
|
54
|
+
)
|
|
55
|
+
except httpx.HTTPError as exc:
|
|
56
|
+
raise PlatformError(f"Could not reach platform at {self.base_url}: {exc}") from exc
|
|
57
|
+
if response.status_code == 401:
|
|
58
|
+
raise PlatformError("Developer key is invalid or revoked", status_code=401)
|
|
59
|
+
if response.status_code >= 400:
|
|
60
|
+
raise PlatformError(
|
|
61
|
+
f"Account discovery failed ({response.status_code})", status_code=response.status_code
|
|
62
|
+
)
|
|
63
|
+
payload = response.json()
|
|
64
|
+
return payload if isinstance(payload, list) else []
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""`refactor account` commands for account context selection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from refactorai_cli.auth import AuthError, ensure_authenticated
|
|
10
|
+
from refactorai_cli.client import PlatformClient, PlatformError
|
|
11
|
+
from refactorai_cli.credentials import load_credentials, save_credentials
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
name="account",
|
|
16
|
+
help="List and switch account context for the current developer key.",
|
|
17
|
+
no_args_is_help=True,
|
|
18
|
+
add_completion=False,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _discover_accounts() -> tuple[list[dict], dict]:
|
|
23
|
+
try:
|
|
24
|
+
auth_ctx = ensure_authenticated(force_remote=True)
|
|
25
|
+
except AuthError as exc:
|
|
26
|
+
console.print(f"[red]Not authenticated:[/red] {exc}")
|
|
27
|
+
raise typer.Exit(code=1) from exc
|
|
28
|
+
client = PlatformClient()
|
|
29
|
+
try:
|
|
30
|
+
rows = client.list_key_accounts(auth_ctx.key.key)
|
|
31
|
+
except PlatformError as exc:
|
|
32
|
+
console.print(f"[red]Could not load accounts:[/red] {exc}")
|
|
33
|
+
raise typer.Exit(code=1) from exc
|
|
34
|
+
return rows, load_credentials()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.command("list")
|
|
38
|
+
def account_list() -> None:
|
|
39
|
+
rows, creds = _discover_accounts()
|
|
40
|
+
active_slug = str(creds.get("active_account_slug") or "")
|
|
41
|
+
if not rows:
|
|
42
|
+
console.print("[dim]No accounts available for this key.[/dim]")
|
|
43
|
+
return
|
|
44
|
+
table = Table(title="Accounts")
|
|
45
|
+
table.add_column("current")
|
|
46
|
+
table.add_column("slug")
|
|
47
|
+
table.add_column("name")
|
|
48
|
+
table.add_column("type")
|
|
49
|
+
table.add_column("role")
|
|
50
|
+
for row in rows:
|
|
51
|
+
slug = str(row.get("slug") or "")
|
|
52
|
+
table.add_row("*" if slug and slug == active_slug else "", slug, str(row.get("display_name") or ""), str(row.get("type") or ""), str(row.get("role") or ""))
|
|
53
|
+
console.print(table)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.command("use")
|
|
57
|
+
def account_use(
|
|
58
|
+
slug: str = typer.Argument(..., help="Account slug to set active."),
|
|
59
|
+
) -> None:
|
|
60
|
+
rows, creds = _discover_accounts()
|
|
61
|
+
selected = next((row for row in rows if str(row.get("slug") or "").lower() == slug.lower()), None)
|
|
62
|
+
if selected is None:
|
|
63
|
+
console.print(f"[red]Account not available for this key:[/red] {slug}")
|
|
64
|
+
raise typer.Exit(code=1)
|
|
65
|
+
creds["active_account_id"] = selected.get("id")
|
|
66
|
+
creds["active_account_slug"] = selected.get("slug")
|
|
67
|
+
save_credentials(creds)
|
|
68
|
+
console.print(f"[green]Active account set:[/green] {selected.get('slug')} ({selected.get('display_name')})")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@app.command("current")
|
|
72
|
+
def account_current() -> None:
|
|
73
|
+
creds = load_credentials()
|
|
74
|
+
slug = creds.get("active_account_slug") or "<unset>"
|
|
75
|
+
account_id = creds.get("active_account_id") or "<unset>"
|
|
76
|
+
console.print(f"[bold]active_account_slug[/bold]: {slug}\n[bold]active_account_id[/bold]: {account_id}")
|
|
@@ -43,6 +43,8 @@ def login(
|
|
|
43
43
|
{
|
|
44
44
|
"developer_key": developer_key,
|
|
45
45
|
"account_id": payload.get("account_id"),
|
|
46
|
+
"active_account_id": payload.get("account_id"),
|
|
47
|
+
"active_account_slug": payload.get("account_slug"),
|
|
46
48
|
"platform_url": platform_url(),
|
|
47
49
|
"masked_key": mask_key(developer_key),
|
|
48
50
|
}
|
|
@@ -80,6 +82,7 @@ def whoami() -> None:
|
|
|
80
82
|
suffix = " (cached)" if ctx.cached else ""
|
|
81
83
|
console.print(
|
|
82
84
|
f"[bold]account[/bold]: {ctx.account_id}\n"
|
|
85
|
+
f"[bold]active_account_slug[/bold]: {load_credentials().get('active_account_slug') or 'unknown'}\n"
|
|
83
86
|
f"[bold]key[/bold]: {mask_key(ctx.key.key)} (source: {source})\n"
|
|
84
87
|
f"[bold]quota_remaining[/bold]: {quota}{suffix}"
|
|
85
88
|
)
|
|
@@ -53,6 +53,13 @@ from refactor_core.apply import (
|
|
|
53
53
|
from refactor_core.budgeting import compute_budget
|
|
54
54
|
from refactor_core.capabilities import resolve_capabilities
|
|
55
55
|
from refactor_core.gate import evaluate_gate
|
|
56
|
+
from refactor_core.merge_gate import (
|
|
57
|
+
build_merge_contract,
|
|
58
|
+
merge_contract_result,
|
|
59
|
+
resolve_merge_gate_settings,
|
|
60
|
+
run_l4_adapter,
|
|
61
|
+
should_run_l4,
|
|
62
|
+
)
|
|
56
63
|
from refactor_core.refactor_requests import (
|
|
57
64
|
DEFERRED,
|
|
58
65
|
RequestSuppression,
|
|
@@ -68,7 +75,11 @@ from refactor_core.refactor_ops import remediation_hint
|
|
|
68
75
|
from refactor_core.indexing import index_target
|
|
69
76
|
from refactor_core.models import Finding, GeneratedTest, PatchGroup, RunArtifact
|
|
70
77
|
from refactor_core.rules import annotate_documents_with_rules, filter_documents_by_rules
|
|
71
|
-
from refactor_core.security import
|
|
78
|
+
from refactor_core.security import (
|
|
79
|
+
compute_semgrep_security_summary,
|
|
80
|
+
merge_refactor_findings,
|
|
81
|
+
suppress_findings_for_rr,
|
|
82
|
+
)
|
|
72
83
|
from refactor_core.security.semgrep_runner import EMBEDDED_SEMGREP_SENTINEL
|
|
73
84
|
from refactor_core.security.suppression import SecuritySuppression
|
|
74
85
|
from refactor_core.store import (
|
|
@@ -635,7 +646,8 @@ def _cloud_inference(
|
|
|
635
646
|
model_hint: str | None = None,
|
|
636
647
|
findings: list | None = None,
|
|
637
648
|
) -> RunArtifact:
|
|
638
|
-
|
|
649
|
+
_raw_alerts = constitution.get_setting("max_alerts_per_run", 5)
|
|
650
|
+
max_alerts = int(_raw_alerts if _raw_alerts is not None else 5)
|
|
639
651
|
gate_mode = str(constitution.get_setting("gate_mode", "strict") or "strict")
|
|
640
652
|
excludes = set(constitution.get_setting("exclude", []) or [])
|
|
641
653
|
documents = index_target(Path(project_root), target, excludes=excludes)
|
|
@@ -709,13 +721,40 @@ def _cloud_inference(
|
|
|
709
721
|
raise RuntimeError(f"Cloud inference failed ({response.status_code}): {response.text[:500]}")
|
|
710
722
|
|
|
711
723
|
data = response.json() if response.content else {}
|
|
712
|
-
|
|
713
|
-
|
|
724
|
+
_no_cap = max_alerts <= 0
|
|
725
|
+
findings = [Finding.model_validate(item) for item in (data.get("findings") or [])]
|
|
726
|
+
patches = [PatchGroup.model_validate(item) for item in (data.get("patches") or [])]
|
|
714
727
|
generated_tests = [
|
|
715
728
|
GeneratedTest.model_validate(item) for item in (data.get("generated_tests") or [])
|
|
716
|
-
]
|
|
729
|
+
]
|
|
730
|
+
if not _no_cap:
|
|
731
|
+
findings = findings[:max_alerts]
|
|
732
|
+
patches = patches[:max_alerts]
|
|
733
|
+
generated_tests = generated_tests[:max_alerts]
|
|
717
734
|
gate = evaluate_gate(findings, [], mode=gate_mode)
|
|
718
735
|
|
|
736
|
+
# Optional security-scan enrichment (R19). Semgrep is an additive signal and
|
|
737
|
+
# strictly fail-open: it never blocks the core cloud run and the gate stays
|
|
738
|
+
# driven by model findings only. Prefer the server-side baseline (Phase 2)
|
|
739
|
+
# when the platform ran it; otherwise fall back to a client-side scan against
|
|
740
|
+
# the local checkout (Phase 1) so parity holds against older backends.
|
|
741
|
+
security_summary: dict = {}
|
|
742
|
+
security_notes: list[str] = []
|
|
743
|
+
server_summary = data.get("security_summary")
|
|
744
|
+
if isinstance(server_summary, dict) and server_summary.get("enabled"):
|
|
745
|
+
security_summary = server_summary
|
|
746
|
+
else:
|
|
747
|
+
try:
|
|
748
|
+
security_summary, security_notes = compute_semgrep_security_summary(
|
|
749
|
+
project_root=Path(project_root),
|
|
750
|
+
constitution=constitution,
|
|
751
|
+
target=target,
|
|
752
|
+
base_findings=findings,
|
|
753
|
+
)
|
|
754
|
+
except Exception as exc: # optional feature must never fail the cloud run
|
|
755
|
+
security_summary = {}
|
|
756
|
+
security_notes = [f"Security baseline scan skipped (unexpected error): {exc}"]
|
|
757
|
+
|
|
719
758
|
run_id = uuid.uuid4().hex[:12]
|
|
720
759
|
if stage == "review":
|
|
721
760
|
summary = (
|
|
@@ -736,9 +775,11 @@ def _cloud_inference(
|
|
|
736
775
|
summary=summary,
|
|
737
776
|
constitution_hash=constitution.content_hash,
|
|
738
777
|
model_id=str(data.get("model_id") or payload["model_hint"]),
|
|
778
|
+
notes=security_notes,
|
|
739
779
|
rule_source=rule_meta.get("rule_source", ""),
|
|
740
780
|
rule_pattern=rule_meta.get("rule_pattern", ""),
|
|
741
781
|
rule_hash=rule_meta.get("rule_hash", ""),
|
|
782
|
+
security_summary=security_summary,
|
|
742
783
|
)
|
|
743
784
|
|
|
744
785
|
|
|
@@ -1893,6 +1934,7 @@ def review(
|
|
|
1893
1934
|
artifact=artifact,
|
|
1894
1935
|
)
|
|
1895
1936
|
_apply_compliance_gate_to_artifact(artifact)
|
|
1937
|
+
_apply_merge_contract_to_artifact(artifact, project_root, project_config)
|
|
1896
1938
|
run_store.write_artifact(artifact, target=target, status="completed")
|
|
1897
1939
|
if compliance_frameworks:
|
|
1898
1940
|
summary = dict(getattr(artifact, "compliance_summary", {}) or {})
|
|
@@ -2109,6 +2151,7 @@ def code(
|
|
|
2109
2151
|
artifact=review_artifact,
|
|
2110
2152
|
)
|
|
2111
2153
|
_apply_compliance_gate_to_artifact(review_artifact)
|
|
2154
|
+
_apply_merge_contract_to_artifact(review_artifact, project_root, project_config)
|
|
2112
2155
|
if compliance_frameworks:
|
|
2113
2156
|
summary = dict(getattr(review_artifact, "compliance_summary", {}) or {})
|
|
2114
2157
|
console.print(
|
|
@@ -2685,6 +2728,94 @@ def _apply_compliance_gate_to_artifact(artifact: RunArtifact) -> None:
|
|
|
2685
2728
|
artifact.gate.blocking_reasons["reasons"] = reasons
|
|
2686
2729
|
|
|
2687
2730
|
|
|
2731
|
+
def _detect_changed_files(project_root: Path) -> list[str] | None:
|
|
2732
|
+
"""Best-effort list of changed files via git (staged + unstaged + untracked).
|
|
2733
|
+
|
|
2734
|
+
Returns ``None`` when git is unavailable or the path is not a repo, so the
|
|
2735
|
+
change-size guardrail degrades to "unknown" rather than a false value.
|
|
2736
|
+
"""
|
|
2737
|
+
try:
|
|
2738
|
+
proc = subprocess.run(
|
|
2739
|
+
["git", "status", "--porcelain"],
|
|
2740
|
+
cwd=str(project_root),
|
|
2741
|
+
capture_output=True,
|
|
2742
|
+
text=True,
|
|
2743
|
+
timeout=15,
|
|
2744
|
+
check=False,
|
|
2745
|
+
)
|
|
2746
|
+
except (OSError, subprocess.SubprocessError):
|
|
2747
|
+
return None
|
|
2748
|
+
if proc.returncode != 0:
|
|
2749
|
+
return None
|
|
2750
|
+
files: list[str] = []
|
|
2751
|
+
for line in (proc.stdout or "").splitlines():
|
|
2752
|
+
name = line[3:].strip() if len(line) > 3 else line.strip()
|
|
2753
|
+
if " -> " in name: # rename: take the destination path
|
|
2754
|
+
name = name.split(" -> ", 1)[1].strip()
|
|
2755
|
+
if name:
|
|
2756
|
+
files.append(name)
|
|
2757
|
+
return files
|
|
2758
|
+
|
|
2759
|
+
|
|
2760
|
+
def _agent_meta_from_env() -> dict:
|
|
2761
|
+
"""Resolve agent-authorship metadata from the environment (CI-friendly)."""
|
|
2762
|
+
authored = os.environ.get("REFACTOR_AGENT_AUTHORED", "").strip().lower() in {
|
|
2763
|
+
"1",
|
|
2764
|
+
"true",
|
|
2765
|
+
"yes",
|
|
2766
|
+
"on",
|
|
2767
|
+
}
|
|
2768
|
+
risk_tier = os.environ.get("REFACTOR_AGENT_RISK_TIER", "").strip().lower()
|
|
2769
|
+
return {"authored": authored, "risk_tier": risk_tier}
|
|
2770
|
+
|
|
2771
|
+
|
|
2772
|
+
def _apply_merge_contract_to_artifact(
|
|
2773
|
+
artifact: RunArtifact, project_root: Path, project_config
|
|
2774
|
+
) -> None:
|
|
2775
|
+
"""Assemble the R24 merge contract from existing run outcomes.
|
|
2776
|
+
|
|
2777
|
+
Runs identically for every execution mode (local/cloud_managed/cloud_byok):
|
|
2778
|
+
the CLI always assembles the artifact and persists the report locally, so the
|
|
2779
|
+
contract is computed once here from the gate/compliance/security signals.
|
|
2780
|
+
|
|
2781
|
+
The expensive real-system (L4) layer runs only when it is required (strict
|
|
2782
|
+
profile, agent policy, or an explicit ``REFACTOR_MERGE_RUN_L4`` opt-in) so the
|
|
2783
|
+
fast inner loop stays cheap (R24 Phase 2).
|
|
2784
|
+
"""
|
|
2785
|
+
settings = resolve_merge_gate_settings(
|
|
2786
|
+
dict(getattr(project_config, "settings", {}) or {})
|
|
2787
|
+
)
|
|
2788
|
+
agent_meta = _agent_meta_from_env()
|
|
2789
|
+
changed_files = _detect_changed_files(project_root)
|
|
2790
|
+
changed_count = len(changed_files) if isinstance(changed_files, list) else None
|
|
2791
|
+
|
|
2792
|
+
l4_verdict = None
|
|
2793
|
+
if should_run_l4(settings, agent_authored=bool(agent_meta.get("authored"))):
|
|
2794
|
+
console.print("[dim]merge gate: running real-system (L4) validation...[/dim]")
|
|
2795
|
+
try:
|
|
2796
|
+
l4_verdict = run_l4_adapter(
|
|
2797
|
+
settings,
|
|
2798
|
+
project_root=project_root,
|
|
2799
|
+
run_id=str(getattr(artifact, "run_id", "") or ""),
|
|
2800
|
+
constitution_hash=str(getattr(artifact, "constitution_hash", "") or ""),
|
|
2801
|
+
changed_files=changed_files or [],
|
|
2802
|
+
)
|
|
2803
|
+
except Exception as exc: # adapter must never crash the run
|
|
2804
|
+
l4_verdict = {"status": "error", "reason_code": f"l4_adapter_error: {exc}"[:200]}
|
|
2805
|
+
|
|
2806
|
+
contract = build_merge_contract(
|
|
2807
|
+
gate=artifact.gate.model_dump(),
|
|
2808
|
+
compliance_summary=dict(getattr(artifact, "compliance_summary", {}) or {}),
|
|
2809
|
+
security_summary=dict(getattr(artifact, "security_summary", {}) or {}),
|
|
2810
|
+
settings=settings,
|
|
2811
|
+
constitution_hash=str(getattr(artifact, "constitution_hash", "") or ""),
|
|
2812
|
+
agent_meta=agent_meta,
|
|
2813
|
+
l4_verdict=l4_verdict,
|
|
2814
|
+
changed_files_count=changed_count,
|
|
2815
|
+
)
|
|
2816
|
+
artifact.merge_contract = contract
|
|
2817
|
+
|
|
2818
|
+
|
|
2688
2819
|
def _report_framework_suppressions(
|
|
2689
2820
|
suppressions: list[RequestSuppression], *, artifact: RunArtifact | None = None
|
|
2690
2821
|
) -> None:
|
|
@@ -3002,6 +3133,11 @@ def check(
|
|
|
3002
3133
|
"--compliance-gate",
|
|
3003
3134
|
help="Also enforce latest run compliance gate status (for CI/PR pipelines).",
|
|
3004
3135
|
),
|
|
3136
|
+
merge_gate: bool = typer.Option(
|
|
3137
|
+
False,
|
|
3138
|
+
"--merge-gate",
|
|
3139
|
+
help="Also enforce the latest run merge contract result (for CI/PR pipelines).",
|
|
3140
|
+
),
|
|
3005
3141
|
) -> None:
|
|
3006
3142
|
"""Check the constitution for committed secrets (developer/provider keys)."""
|
|
3007
3143
|
files = find_project_files()
|
|
@@ -3013,6 +3149,7 @@ def check(
|
|
|
3013
3149
|
config = load_config(config_path)
|
|
3014
3150
|
findings = raw_secret_matches(constitution) + raw_secret_matches_config(config)
|
|
3015
3151
|
compliance_failed = False
|
|
3152
|
+
merge_failed = False
|
|
3016
3153
|
if compliance_gate:
|
|
3017
3154
|
run_dir = head_run_dir(consti_path.parent)
|
|
3018
3155
|
if run_dir is None:
|
|
@@ -3031,6 +3168,29 @@ def check(
|
|
|
3031
3168
|
)
|
|
3032
3169
|
else:
|
|
3033
3170
|
console.print("[green]Compliance gate[/green]: passed")
|
|
3171
|
+
if merge_gate:
|
|
3172
|
+
run_dir = head_run_dir(consti_path.parent)
|
|
3173
|
+
if run_dir is None:
|
|
3174
|
+
console.print("[yellow]Merge gate check skipped:[/yellow] no runs yet.")
|
|
3175
|
+
else:
|
|
3176
|
+
report = read_report(run_dir) or {}
|
|
3177
|
+
contract = report.get("merge_contract", {}) if isinstance(report, dict) else {}
|
|
3178
|
+
result = merge_contract_result(contract)
|
|
3179
|
+
if result == "blocked":
|
|
3180
|
+
merge_failed = True
|
|
3181
|
+
reasons = contract.get("blocking_reasons") or ["merge_contract_blocked"]
|
|
3182
|
+
console.print(
|
|
3183
|
+
"[red]Merge gate blocked[/red] "
|
|
3184
|
+
f"(profile={contract.get('profile', 'unknown')}): "
|
|
3185
|
+
+ ", ".join(str(r) for r in reasons)
|
|
3186
|
+
)
|
|
3187
|
+
elif result == "passed":
|
|
3188
|
+
console.print(
|
|
3189
|
+
f"[green]Merge gate[/green]: passed "
|
|
3190
|
+
f"(profile={contract.get('profile', 'unknown')})"
|
|
3191
|
+
)
|
|
3192
|
+
else:
|
|
3193
|
+
console.print("[yellow]Merge gate[/yellow]: no contract in latest run.")
|
|
3034
3194
|
if findings:
|
|
3035
3195
|
kinds = ", ".join(sorted({kind for kind, _ in findings}))
|
|
3036
3196
|
console.print(
|
|
@@ -3038,11 +3198,99 @@ def check(
|
|
|
3038
3198
|
f"{consti_path} or {config_path}. Replace secrets with `${{ENV_VAR}}` references."
|
|
3039
3199
|
)
|
|
3040
3200
|
raise typer.Exit(code=1 if strict else 0)
|
|
3041
|
-
if compliance_failed:
|
|
3201
|
+
if compliance_failed or merge_failed:
|
|
3042
3202
|
raise typer.Exit(code=1 if strict else 0)
|
|
3043
3203
|
console.print("[green]OK[/green]: no committed secret-like literals detected.")
|
|
3044
3204
|
|
|
3045
3205
|
|
|
3206
|
+
def gate() -> None:
|
|
3207
|
+
"""Show the resolved merge contract for the latest run (R24, read-only)."""
|
|
3208
|
+
_require_auth()
|
|
3209
|
+
project_root, _constitution, project_config = _load_project()
|
|
3210
|
+
settings = resolve_merge_gate_settings(
|
|
3211
|
+
dict(getattr(project_config, "settings", {}) or {})
|
|
3212
|
+
)
|
|
3213
|
+
console.print("[bold]Merge gate[/bold]")
|
|
3214
|
+
console.print(
|
|
3215
|
+
f" profile: {settings.profile} "
|
|
3216
|
+
f"security_threshold={settings.security_threshold} "
|
|
3217
|
+
f"require_compliance_pass={settings.require_compliance_pass} "
|
|
3218
|
+
f"l4_adapter={settings.real_system_adapter}"
|
|
3219
|
+
)
|
|
3220
|
+
|
|
3221
|
+
run_dir = head_run_dir(project_root)
|
|
3222
|
+
report = read_report(run_dir) if run_dir else {}
|
|
3223
|
+
contract = (
|
|
3224
|
+
dict((report or {}).get("merge_contract", {}) or {})
|
|
3225
|
+
if isinstance(report, dict)
|
|
3226
|
+
else {}
|
|
3227
|
+
)
|
|
3228
|
+
if not contract:
|
|
3229
|
+
console.print(
|
|
3230
|
+
"[yellow]No merge contract in the latest run.[/yellow] "
|
|
3231
|
+
"Run `refactor review` to produce one."
|
|
3232
|
+
)
|
|
3233
|
+
return
|
|
3234
|
+
|
|
3235
|
+
layers = contract.get("layers", {}) if isinstance(contract.get("layers"), dict) else {}
|
|
3236
|
+
table = Table(title="Layers of confidence", show_lines=False)
|
|
3237
|
+
table.add_column("layer", no_wrap=True)
|
|
3238
|
+
table.add_column("enforcement", no_wrap=True)
|
|
3239
|
+
table.add_column("status", no_wrap=True)
|
|
3240
|
+
table.add_column("reason", no_wrap=True)
|
|
3241
|
+
for layer_id in ("well_formed", "internally_correct", "boundary_compatible", "system_correct"):
|
|
3242
|
+
entry = layers.get(layer_id, {}) if isinstance(layers.get(layer_id), dict) else {}
|
|
3243
|
+
code = str(entry.get("code", "")) or layer_id
|
|
3244
|
+
table.add_row(
|
|
3245
|
+
f"{code} {layer_id}",
|
|
3246
|
+
str(entry.get("class", "off")),
|
|
3247
|
+
str(entry.get("status", "unknown")),
|
|
3248
|
+
str(entry.get("reason_code", "") or "-"),
|
|
3249
|
+
)
|
|
3250
|
+
console.print(table)
|
|
3251
|
+
|
|
3252
|
+
# Real-system (L4) verdict details, when the adapter ran.
|
|
3253
|
+
l4 = layers.get("system_correct", {}) if isinstance(layers.get("system_correct"), dict) else {}
|
|
3254
|
+
nf = l4.get("non_functional", {}) if isinstance(l4.get("non_functional"), dict) else {}
|
|
3255
|
+
if l4.get("status") in {"passed", "failed", "error"} or nf.get("within_budget") is not None:
|
|
3256
|
+
parts = [f"status={l4.get('status', 'unknown')}"]
|
|
3257
|
+
if nf.get("p95_latency_ms") is not None:
|
|
3258
|
+
parts.append(f"p95={nf.get('p95_latency_ms')}ms")
|
|
3259
|
+
if nf.get("error_rate_pct") is not None:
|
|
3260
|
+
parts.append(f"error_rate={nf.get('error_rate_pct')}%")
|
|
3261
|
+
if nf.get("within_budget") is not None:
|
|
3262
|
+
parts.append(f"within_budget={nf.get('within_budget')}")
|
|
3263
|
+
console.print(" L4 real-system: " + " ".join(parts))
|
|
3264
|
+
evidence = str(l4.get("evidence_url", "") or "")
|
|
3265
|
+
if evidence:
|
|
3266
|
+
console.print(f" evidence: {evidence}")
|
|
3267
|
+
|
|
3268
|
+
agent = contract.get("agent", {}) if isinstance(contract.get("agent"), dict) else {}
|
|
3269
|
+
if agent.get("authored"):
|
|
3270
|
+
console.print(
|
|
3271
|
+
f" agent: authored=true risk_tier={agent.get('risk_tier', '') or 'unknown'} "
|
|
3272
|
+
f"require_l4={agent.get('require_l4', False)}"
|
|
3273
|
+
)
|
|
3274
|
+
changed = contract.get("changed_files_count")
|
|
3275
|
+
if isinstance(changed, int):
|
|
3276
|
+
console.print(f" changed_files={changed}")
|
|
3277
|
+
|
|
3278
|
+
result = merge_contract_result(contract)
|
|
3279
|
+
result_color = "green" if result == "passed" else ("red" if result == "blocked" else "yellow")
|
|
3280
|
+
console.print(
|
|
3281
|
+
f" quality_gate={contract.get('quality_gate', 'unknown')} "
|
|
3282
|
+
f"compliance={contract.get('compliance', 'unknown')} "
|
|
3283
|
+
f"security_findings={int((contract.get('security', {}) or {}).get('normalized_count', 0) or 0)}"
|
|
3284
|
+
)
|
|
3285
|
+
console.print(f" result: [{result_color}]{result}[/{result_color}]")
|
|
3286
|
+
reasons = contract.get("blocking_reasons") or []
|
|
3287
|
+
if reasons:
|
|
3288
|
+
console.print(" blocking_reasons: " + ", ".join(str(r) for r in reasons))
|
|
3289
|
+
consti_hash = str(contract.get("constitution_hash", "") or "")
|
|
3290
|
+
if consti_hash:
|
|
3291
|
+
console.print(f" [dim]constitution_hash={consti_hash}[/dim]")
|
|
3292
|
+
|
|
3293
|
+
|
|
3046
3294
|
def _mask_secret(value: str) -> str:
|
|
3047
3295
|
value = value.strip()
|
|
3048
3296
|
if len(value) <= 8:
|
|
@@ -219,3 +219,10 @@ def config(ctx: typer.Context) -> None:
|
|
|
219
219
|
def compliance(ctx: typer.Context) -> None:
|
|
220
220
|
"""Inspect compliance packs/coverage via proprietary runtime."""
|
|
221
221
|
_exec_runtime("compliance", list(ctx.args))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def gate(ctx: typer.Context) -> None:
|
|
225
|
+
"""Show the latest run's merge contract (read-only; handled locally)."""
|
|
226
|
+
# The merge contract is read from the local run report and needs no
|
|
227
|
+
# proprietary runtime, so it is handled locally for every execution mode.
|
|
228
|
+
_delegate_to_run_cmds("gate", list(ctx.args))
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
5
8
|
import typer
|
|
6
9
|
|
|
7
10
|
from refactorai_cli import __version__
|
|
8
11
|
from refactorai_cli.commands import (
|
|
12
|
+
account_cmds,
|
|
9
13
|
auth_cmds,
|
|
10
14
|
cloud_cmds,
|
|
11
15
|
engine_cmds,
|
|
@@ -23,6 +27,43 @@ app = typer.Typer(
|
|
|
23
27
|
)
|
|
24
28
|
|
|
25
29
|
|
|
30
|
+
_INTRO_COMMANDS = {"setup", "doctor", "review", "code"}
|
|
31
|
+
_TIP_BY_COMMAND = {
|
|
32
|
+
"setup": "Next: refactor doctor",
|
|
33
|
+
"doctor": "Next: refactor review .",
|
|
34
|
+
"review": "Next: refactor code . --apply",
|
|
35
|
+
"code": "Next: refactor review .",
|
|
36
|
+
}
|
|
37
|
+
_TITLE_BY_COMMAND = {
|
|
38
|
+
"setup": "Refactor Setup Agent",
|
|
39
|
+
"doctor": "Refactor Doctor Agent",
|
|
40
|
+
"review": "Refactor Review Agent",
|
|
41
|
+
"code": "Refactor Code Agent",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _print_agent_intro(command_name: str) -> None:
|
|
46
|
+
# Keep this lightweight and TTY-only so scripts/non-interactive usage are unchanged.
|
|
47
|
+
if not sys.stdout.isatty():
|
|
48
|
+
return
|
|
49
|
+
title = _TITLE_BY_COMMAND.get(command_name, "Refactor Agent")
|
|
50
|
+
tip = _TIP_BY_COMMAND.get(command_name, "Next: refactor review .")
|
|
51
|
+
green_bold = "\033[1;38;5;46m"
|
|
52
|
+
green_soft = "\033[38;5;120m"
|
|
53
|
+
green_art = "\033[38;5;82m"
|
|
54
|
+
reset = "\033[0m"
|
|
55
|
+
typer.echo("")
|
|
56
|
+
typer.echo(f"{green_bold}{title}{reset}")
|
|
57
|
+
typer.echo(f"{green_soft}Model: Sonnet 4.5{reset}")
|
|
58
|
+
typer.echo(f"Welcome back, {os.environ.get('USER', 'developer')}")
|
|
59
|
+
typer.echo("")
|
|
60
|
+
typer.echo(f"{green_art} * [■ ■] *{reset}")
|
|
61
|
+
typer.echo(f"{green_art} * \\_/ *{reset}")
|
|
62
|
+
typer.echo("")
|
|
63
|
+
typer.echo(f"{green_bold}Tip:{reset} {tip}")
|
|
64
|
+
typer.echo("")
|
|
65
|
+
|
|
66
|
+
|
|
26
67
|
def _version_callback(value: bool) -> None:
|
|
27
68
|
if value:
|
|
28
69
|
typer.echo(f"refactor {__version__}")
|
|
@@ -31,6 +72,7 @@ def _version_callback(value: bool) -> None:
|
|
|
31
72
|
|
|
32
73
|
@app.callback()
|
|
33
74
|
def main(
|
|
75
|
+
ctx: typer.Context,
|
|
34
76
|
version: bool = typer.Option(
|
|
35
77
|
False,
|
|
36
78
|
"--version",
|
|
@@ -41,11 +83,15 @@ def main(
|
|
|
41
83
|
),
|
|
42
84
|
) -> None:
|
|
43
85
|
"""refactor CLI."""
|
|
86
|
+
command_name = (ctx.invoked_subcommand or "").strip().lower()
|
|
87
|
+
if command_name in _INTRO_COMMANDS:
|
|
88
|
+
_print_agent_intro(command_name)
|
|
44
89
|
|
|
45
90
|
|
|
46
91
|
# Auth (R7.1)
|
|
47
92
|
app.command()(auth_cmds.login)
|
|
48
93
|
app.command()(auth_cmds.whoami)
|
|
94
|
+
app.add_typer(account_cmds.app, name="account")
|
|
49
95
|
|
|
50
96
|
# Project / run surface (proxied to activated runtime artifact)
|
|
51
97
|
_RUNTIME_CTX = {"allow_extra_args": True, "ignore_unknown_options": True}
|
|
@@ -65,6 +111,7 @@ app.command(context_settings=_RUNTIME_CTX)(runtime_proxy_cmds.check)
|
|
|
65
111
|
app.command(context_settings=_RUNTIME_CTX)(runtime_proxy_cmds.doctor)
|
|
66
112
|
app.command(context_settings=_RUNTIME_CTX)(runtime_proxy_cmds.config)
|
|
67
113
|
app.command(context_settings=_RUNTIME_CTX)(runtime_proxy_cmds.compliance)
|
|
114
|
+
app.command(context_settings=_RUNTIME_CTX)(runtime_proxy_cmds.gate)
|
|
68
115
|
|
|
69
116
|
app.command()(setup_cmds.setup)
|
|
70
117
|
|
|
@@ -21,6 +21,7 @@ refactorai_cli.egg-info/entry_points.txt
|
|
|
21
21
|
refactorai_cli.egg-info/requires.txt
|
|
22
22
|
refactorai_cli.egg-info/top_level.txt
|
|
23
23
|
refactorai_cli/commands/__init__.py
|
|
24
|
+
refactorai_cli/commands/account_cmds.py
|
|
24
25
|
refactorai_cli/commands/auth_cmds.py
|
|
25
26
|
refactorai_cli/commands/cloud_cmds.py
|
|
26
27
|
refactorai_cli/commands/engine_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
|