refactorai-cli 0.2.15__tar.gz → 0.2.17__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 (32) hide show
  1. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/PKG-INFO +1 -1
  2. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/pyproject.toml +1 -1
  3. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/__init__.py +1 -1
  4. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/commands/run_cmds.py +550 -6
  5. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/commands/runtime_cmds.py +18 -1
  6. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/commands/runtime_proxy_cmds.py +5 -0
  7. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/main.py +1 -0
  8. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/runtime_manager.py +38 -5
  9. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/setup_flow.py +21 -1
  10. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli.egg-info/PKG-INFO +1 -1
  11. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/README.md +0 -0
  12. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/auth.py +0 -0
  13. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/client.py +0 -0
  14. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/commands/__init__.py +0 -0
  15. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/commands/auth_cmds.py +0 -0
  16. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/commands/engine_cmds.py +0 -0
  17. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/commands/model_cmds.py +0 -0
  18. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/commands/rules_cmds.py +0 -0
  19. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/commands/setup_cmds.py +0 -0
  20. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/control_plane.py +0 -0
  21. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/credentials.py +0 -0
  22. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/local_constitution.py +0 -0
  23. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/local_engine_runtime.py +0 -0
  24. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/local_paths.py +0 -0
  25. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/model_policy.py +0 -0
  26. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli/settings.py +0 -0
  27. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli.egg-info/SOURCES.txt +0 -0
  28. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli.egg-info/dependency_links.txt +0 -0
  29. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli.egg-info/entry_points.txt +0 -0
  30. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli.egg-info/requires.txt +0 -0
  31. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/refactorai_cli.egg-info/top_level.txt +0 -0
  32. {refactorai_cli-0.2.15 → refactorai_cli-0.2.17}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.2.15
3
+ Version: 0.2.17
4
4
  Summary: Local-first CLI for the refactor platform
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "refactorai-cli"
3
- version = "0.2.15"
3
+ version = "0.2.17"
4
4
  description = "Local-first CLI for the refactor platform"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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.2.15"
8
+ __version__ = "0.2.17"
@@ -35,6 +35,10 @@ from refactor_core.constitution import (
35
35
  raw_secret_matches,
36
36
  raw_secret_matches_config,
37
37
  )
38
+ from refactor_core.compliance_settings import resolve_compliance_selection
39
+ from refactor_core.compliance.loader import CompliancePackError, resolve_compliance_packs
40
+ from refactor_core.compliance.models import CompliancePackResolution
41
+ from refactor_core.compliance.evidence import build_compliance_evidence_bundle, evaluate_compliance_gate
38
42
  from refactor_core.apply import (
39
43
  ApplyError,
40
44
  apply_changes,
@@ -50,6 +54,8 @@ from refactor_core.execution.structural_ops import structural_compatibility_matr
50
54
  from refactor_core.gate import evaluate_gate
51
55
  from refactor_core.refactor_requests import (
52
56
  DEFERRED,
57
+ RequestSuppression,
58
+ RequestWarning,
53
59
  clear_requests,
54
60
  create_requests,
55
61
  list_requests,
@@ -659,6 +665,35 @@ def _warn_raw_key(constitution, config) -> None:
659
665
  )
660
666
 
661
667
 
668
+ def _resolve_compliance_context(
669
+ *,
670
+ project_config,
671
+ cli_frameworks: list[str] | None = None,
672
+ cli_level: str | None = None,
673
+ cli_rollout: str | None = None,
674
+ cli_canary_percent: int | None = None,
675
+ ) -> tuple[tuple[str, ...], str, CompliancePackResolution, str, int]:
676
+ """Resolve compliance selection + managed pack set for this run."""
677
+ selection = resolve_compliance_selection(
678
+ dict(getattr(project_config, "settings", {}) or {}),
679
+ cli_frameworks=cli_frameworks,
680
+ cli_level=cli_level,
681
+ cli_rollout=cli_rollout,
682
+ cli_canary_percent=cli_canary_percent,
683
+ )
684
+ resolution = resolve_compliance_packs(
685
+ selection.frameworks,
686
+ level=selection.level,
687
+ )
688
+ return (
689
+ selection.frameworks,
690
+ selection.level,
691
+ resolution,
692
+ selection.rollout,
693
+ selection.canary_percent,
694
+ )
695
+
696
+
662
697
  def init(
663
698
  force: bool = typer.Option(
664
699
  False,
@@ -1479,10 +1514,61 @@ def review(
1479
1514
  "--force",
1480
1515
  help="Run review even if unapplied patches exist from a prior code run.",
1481
1516
  ),
1517
+ compliance_framework: list[str] | None = typer.Option(
1518
+ None,
1519
+ "--compliance-framework",
1520
+ help="Compliance framework to enable for this run. Repeat for multiple values.",
1521
+ ),
1522
+ compliance_level: str | None = typer.Option(
1523
+ None,
1524
+ "--compliance-level",
1525
+ help="Compliance level override: off | advisory | standard | strict.",
1526
+ ),
1527
+ compliance_rollout: str | None = typer.Option(
1528
+ None,
1529
+ "--compliance-rollout",
1530
+ help="Compliance rollout mode: default | staged | canary | off | ga.",
1531
+ ),
1532
+ compliance_canary_percent: int | None = typer.Option(
1533
+ None,
1534
+ "--compliance-canary-percent",
1535
+ help="Compliance canary percentage (0-100) when rollout=canary.",
1536
+ ),
1482
1537
  ) -> None:
1483
1538
  """Run review (findings only) and store the result. Requires a valid key."""
1484
1539
  _require_auth()
1485
1540
  project_root, constitution, project_config = _load_project()
1541
+ try:
1542
+ (
1543
+ compliance_frameworks,
1544
+ resolved_compliance_level,
1545
+ compliance_resolution,
1546
+ compliance_rollout_mode,
1547
+ compliance_canary_pct,
1548
+ ) = (
1549
+ _resolve_compliance_context(
1550
+ project_config=project_config,
1551
+ cli_frameworks=compliance_framework,
1552
+ cli_level=compliance_level,
1553
+ cli_rollout=compliance_rollout,
1554
+ cli_canary_percent=compliance_canary_percent,
1555
+ )
1556
+ )
1557
+ except CompliancePackError as exc:
1558
+ console.print(f"[red]review: compliance pack resolution failed:[/red] {exc}")
1559
+ raise typer.Exit(code=1) from exc
1560
+ if compliance_frameworks:
1561
+ pack_label = ", ".join(
1562
+ f"{pack.pack_id}@{pack.version}" for pack in compliance_resolution.packs
1563
+ ) or "(none loaded)"
1564
+ console.print(
1565
+ "[dim]review: compliance selection "
1566
+ f"frameworks={list(compliance_frameworks)} level={resolved_compliance_level} "
1567
+ f"rollout={compliance_rollout_mode} canary_percent={compliance_canary_pct} "
1568
+ f"packs={pack_label}[/dim]"
1569
+ )
1570
+ for warning in compliance_resolution.warnings:
1571
+ console.print(f"[yellow]review: compliance warning:[/yellow] {warning}")
1486
1572
  register_project(project_root, constitution_hash=constitution.content_hash)
1487
1573
  _maybe_build_graph(project_root, project_config)
1488
1574
  sandbox_ctx = None
@@ -1555,9 +1641,39 @@ def review(
1555
1641
  _print_findings(artifact.findings)
1556
1642
 
1557
1643
  # A fresh review supersedes prior refactor requests: replace them with one
1558
- # RR per finding (central-only; nothing is written into the project tree).
1559
- clear_requests(project_root)
1560
- requests = create_requests(project_root, artifact.findings, run_id=artifact.run_id)
1644
+ # RR per actionable finding (central-only; nothing is written into project tree).
1645
+ requests = _replace_requests_with_framework_guardrails(
1646
+ project_root=project_root,
1647
+ findings=artifact.findings,
1648
+ run_id=artifact.run_id,
1649
+ compliance_frameworks=compliance_frameworks,
1650
+ compliance_level=resolved_compliance_level,
1651
+ compliance_rollout=compliance_rollout_mode,
1652
+ compliance_canary_percent=compliance_canary_pct,
1653
+ compliance_resolution=compliance_resolution,
1654
+ artifact=artifact,
1655
+ )
1656
+ _apply_compliance_gate_to_artifact(artifact)
1657
+ run_store.write_artifact(artifact, target=target, status="completed")
1658
+ if compliance_frameworks:
1659
+ artifact.notes.append(
1660
+ "Compliance selection: frameworks="
1661
+ + ",".join(compliance_frameworks)
1662
+ + (
1663
+ f" level={resolved_compliance_level} rollout={compliance_rollout_mode} "
1664
+ f"canary_percent={compliance_canary_pct}"
1665
+ )
1666
+ )
1667
+ if compliance_resolution.packs:
1668
+ artifact.notes.append(
1669
+ "Compliance packs: "
1670
+ + ", ".join(
1671
+ f"{pack.pack_id}@{pack.version}"
1672
+ for pack in compliance_resolution.packs
1673
+ )
1674
+ )
1675
+ for warning in compliance_resolution.warnings:
1676
+ artifact.notes.append(f"Compliance pack warning: {warning}")
1561
1677
  if requests:
1562
1678
  console.print(
1563
1679
  f"\n[bold]Created {len(requests)} refactor request(s).[/bold] "
@@ -1602,6 +1718,26 @@ def code(
1602
1718
  "--equivalence",
1603
1719
  help="Equivalence policy: strict | balanced.",
1604
1720
  ),
1721
+ compliance_framework: list[str] | None = typer.Option(
1722
+ None,
1723
+ "--compliance-framework",
1724
+ help="Compliance framework to enable for this run. Repeat for multiple values.",
1725
+ ),
1726
+ compliance_level: str | None = typer.Option(
1727
+ None,
1728
+ "--compliance-level",
1729
+ help="Compliance level override: off | advisory | standard | strict.",
1730
+ ),
1731
+ compliance_rollout: str | None = typer.Option(
1732
+ None,
1733
+ "--compliance-rollout",
1734
+ help="Compliance rollout mode: default | staged | canary | off | ga.",
1735
+ ),
1736
+ compliance_canary_percent: int | None = typer.Option(
1737
+ None,
1738
+ "--compliance-canary-percent",
1739
+ help="Compliance canary percentage (0-100) when rollout=canary.",
1740
+ ),
1605
1741
  ) -> None:
1606
1742
  """Work through refactor requests one by one, applying & verifying each.
1607
1743
 
@@ -1610,6 +1746,37 @@ def code(
1610
1746
  """
1611
1747
  _require_auth()
1612
1748
  project_root, constitution, project_config = _load_project()
1749
+ try:
1750
+ (
1751
+ compliance_frameworks,
1752
+ resolved_compliance_level,
1753
+ compliance_resolution,
1754
+ compliance_rollout_mode,
1755
+ compliance_canary_pct,
1756
+ ) = (
1757
+ _resolve_compliance_context(
1758
+ project_config=project_config,
1759
+ cli_frameworks=compliance_framework,
1760
+ cli_level=compliance_level,
1761
+ cli_rollout=compliance_rollout,
1762
+ cli_canary_percent=compliance_canary_percent,
1763
+ )
1764
+ )
1765
+ except CompliancePackError as exc:
1766
+ console.print(f"[red]code: compliance pack resolution failed:[/red] {exc}")
1767
+ raise typer.Exit(code=1) from exc
1768
+ if compliance_frameworks:
1769
+ pack_label = ", ".join(
1770
+ f"{pack.pack_id}@{pack.version}" for pack in compliance_resolution.packs
1771
+ ) or "(none loaded)"
1772
+ console.print(
1773
+ "[dim]code: compliance selection "
1774
+ f"frameworks={list(compliance_frameworks)} level={resolved_compliance_level} "
1775
+ f"rollout={compliance_rollout_mode} canary_percent={compliance_canary_pct} "
1776
+ f"packs={pack_label}[/dim]"
1777
+ )
1778
+ for warning in compliance_resolution.warnings:
1779
+ console.print(f"[yellow]code: compliance warning:[/yellow] {warning}")
1613
1780
  register_project(project_root, constitution_hash=constitution.content_hash)
1614
1781
  _maybe_build_graph(project_root, project_config)
1615
1782
  sandbox_ctx = None
@@ -1666,10 +1833,38 @@ def code(
1666
1833
  review_store = create_run(project_root, run_id=review_artifact.run_id)
1667
1834
  review_store.write_artifact(review_artifact, target=target, status="completed")
1668
1835
  _print_findings(review_artifact.findings)
1669
- clear_requests(project_root)
1670
- requests = create_requests(
1671
- project_root, review_artifact.findings, run_id=review_artifact.run_id
1836
+ requests = _replace_requests_with_framework_guardrails(
1837
+ project_root=project_root,
1838
+ findings=review_artifact.findings,
1839
+ run_id=review_artifact.run_id,
1840
+ compliance_frameworks=compliance_frameworks,
1841
+ compliance_level=resolved_compliance_level,
1842
+ compliance_rollout=compliance_rollout_mode,
1843
+ compliance_canary_percent=compliance_canary_pct,
1844
+ compliance_resolution=compliance_resolution,
1845
+ artifact=review_artifact,
1672
1846
  )
1847
+ _apply_compliance_gate_to_artifact(review_artifact)
1848
+ if compliance_frameworks:
1849
+ review_artifact.notes.append(
1850
+ "Compliance selection: frameworks="
1851
+ + ",".join(compliance_frameworks)
1852
+ + (
1853
+ f" level={resolved_compliance_level} rollout={compliance_rollout_mode} "
1854
+ f"canary_percent={compliance_canary_pct}"
1855
+ )
1856
+ )
1857
+ if compliance_resolution.packs:
1858
+ review_artifact.notes.append(
1859
+ "Compliance packs: "
1860
+ + ", ".join(
1861
+ f"{pack.pack_id}@{pack.version}"
1862
+ for pack in compliance_resolution.packs
1863
+ )
1864
+ )
1865
+ for warning in compliance_resolution.warnings:
1866
+ review_artifact.notes.append(f"Compliance pack warning: {warning}")
1867
+ review_store.write_artifact(review_artifact, target=target, status="completed")
1673
1868
  console.print(f"[bold]Created {len(requests)} refactor request(s).[/bold]")
1674
1869
 
1675
1870
  if not requests:
@@ -1818,6 +2013,204 @@ def requests(
1818
2013
  console.print(table)
1819
2014
 
1820
2015
 
2016
+ def compliance(
2017
+ action: str = typer.Argument(
2018
+ "show",
2019
+ help="Compliance view action: status | show | coverage | explain | export",
2020
+ ),
2021
+ subject: str = typer.Argument(
2022
+ "",
2023
+ help="Rule ID when action=explain.",
2024
+ ),
2025
+ compliance_framework: list[str] | None = typer.Option(
2026
+ None,
2027
+ "--compliance-framework",
2028
+ help="Compliance framework selection override. Repeat for multiple values.",
2029
+ ),
2030
+ compliance_level: str | None = typer.Option(
2031
+ None,
2032
+ "--compliance-level",
2033
+ help="Compliance level override: off | advisory | standard | strict.",
2034
+ ),
2035
+ compliance_rollout: str | None = typer.Option(
2036
+ None,
2037
+ "--compliance-rollout",
2038
+ help="Compliance rollout mode override: default | staged | canary | off | ga.",
2039
+ ),
2040
+ compliance_canary_percent: int | None = typer.Option(
2041
+ None,
2042
+ "--compliance-canary-percent",
2043
+ help="Compliance canary percentage override when rollout=canary.",
2044
+ ),
2045
+ output: str = typer.Option(
2046
+ "",
2047
+ "--output",
2048
+ help="Output file path for action=export (defaults to latest run directory).",
2049
+ ),
2050
+ ) -> None:
2051
+ """Show/export compliance packs, coverage, and rule details."""
2052
+ _require_auth()
2053
+ _project_root, _constitution, project_config = _load_project()
2054
+ frameworks, level, resolution, rollout_mode, canary_percent = _resolve_compliance_context(
2055
+ project_config=project_config,
2056
+ cli_frameworks=compliance_framework,
2057
+ cli_level=compliance_level,
2058
+ cli_rollout=compliance_rollout,
2059
+ cli_canary_percent=compliance_canary_percent,
2060
+ )
2061
+ mode = str(action or "show").strip().lower()
2062
+ if mode not in {"status", "show", "coverage", "explain", "export"}:
2063
+ console.print(f"[red]Unknown compliance action:[/red] {action}")
2064
+ raise typer.Exit(code=2)
2065
+ if not frameworks:
2066
+ console.print(
2067
+ "[dim]No compliance frameworks selected. "
2068
+ "Set `compliance.frameworks` in refactor.config or pass --compliance-framework.[/dim]"
2069
+ )
2070
+ return
2071
+
2072
+ if mode == "status":
2073
+ run_dir = head_run_dir(_project_root)
2074
+ report = read_report(run_dir) if run_dir else {}
2075
+ summary = (
2076
+ dict((report or {}).get("compliance_summary", {}) or {})
2077
+ if isinstance(report, dict)
2078
+ else {}
2079
+ )
2080
+ gate = dict((report or {}).get("gate", {}) or {}) if isinstance(report, dict) else {}
2081
+ console.print("[bold]Compliance status[/bold]")
2082
+ console.print(
2083
+ f" selection: frameworks={list(frameworks)} level={level} "
2084
+ f"rollout={rollout_mode} canary_percent={canary_percent}"
2085
+ )
2086
+ table = Table(title="Compliance packs", show_lines=False)
2087
+ table.add_column("pack_id", no_wrap=True)
2088
+ table.add_column("framework", no_wrap=True)
2089
+ table.add_column("version", no_wrap=True)
2090
+ table.add_column("rules", no_wrap=True)
2091
+ for pack in resolution.packs:
2092
+ table.add_row(pack.pack_id, pack.framework, pack.version, str(len(pack.rules)))
2093
+ console.print(table)
2094
+ if not run_dir:
2095
+ console.print(
2096
+ "[yellow]No runs yet.[/yellow] Run `refactor review` to produce compliance evidence."
2097
+ )
2098
+ return
2099
+ run_id = str((report or {}).get("run_id", run_dir.name))
2100
+ created = str((report or {}).get("created_at", ""))
2101
+ gate_status = str(gate.get("status", "unknown"))
2102
+ projected = int(summary.get("projected_count", 0) or 0)
2103
+ blocked = int(summary.get("blocked_count", 0) or 0)
2104
+ warned = int(summary.get("warned_count", 0) or 0)
2105
+ gate_color = "green" if gate_status == "passed" else "red"
2106
+ console.print(
2107
+ f" latest_run: {run_id}" + (f" ({created})" if created else "")
2108
+ )
2109
+ console.print(
2110
+ f" gate: [{gate_color}]{gate_status}[/{gate_color}] "
2111
+ f"projected={projected} blocked={blocked} warned={warned}"
2112
+ )
2113
+ if blocked > 0 and level in {"standard", "strict"}:
2114
+ console.print(
2115
+ "[yellow]next step:[/yellow] resolve blocked controls or run with "
2116
+ "`--compliance-rollout staged` during pilot."
2117
+ )
2118
+ elif projected == 0:
2119
+ console.print(
2120
+ "[dim]No compliance-projected controls were triggered in the latest run.[/dim]"
2121
+ )
2122
+ return
2123
+
2124
+ if mode == "show":
2125
+ console.print(
2126
+ "[bold]Compliance selection[/bold]: "
2127
+ f"frameworks={list(frameworks)} level={level} "
2128
+ f"rollout={rollout_mode} canary_percent={canary_percent}"
2129
+ )
2130
+ table = Table(title="Compliance packs", show_lines=False)
2131
+ table.add_column("pack_id", no_wrap=True)
2132
+ table.add_column("framework", no_wrap=True)
2133
+ table.add_column("version", no_wrap=True)
2134
+ table.add_column("rules", no_wrap=True)
2135
+ table.add_column("depends_on")
2136
+ for pack in resolution.packs:
2137
+ table.add_row(
2138
+ pack.pack_id,
2139
+ pack.framework,
2140
+ pack.version,
2141
+ str(len(pack.rules)),
2142
+ ", ".join(pack.depends_on) if pack.depends_on else "-",
2143
+ )
2144
+ console.print(table)
2145
+ for warning in resolution.warnings:
2146
+ console.print(f"[yellow]warning:[/yellow] {warning}")
2147
+ return
2148
+
2149
+ if mode == "export":
2150
+ run_dir = head_run_dir(_project_root)
2151
+ if not run_dir:
2152
+ console.print("[yellow]No runs available to export compliance evidence.[/yellow]")
2153
+ raise typer.Exit(code=1)
2154
+ report = read_report(run_dir) or {}
2155
+ payload = build_compliance_evidence_bundle(report)
2156
+ destination = Path(output).expanduser() if output.strip() else (run_dir / "compliance-evidence.json")
2157
+ try:
2158
+ destination.parent.mkdir(parents=True, exist_ok=True)
2159
+ destination.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
2160
+ except OSError as exc:
2161
+ console.print(f"[red]Failed to write compliance evidence:[/red] {exc}")
2162
+ raise typer.Exit(code=1) from exc
2163
+ console.print(f"[green]Compliance evidence exported:[/green] {destination}")
2164
+ return
2165
+
2166
+ if mode == "coverage":
2167
+ table = Table(title="Compliance coverage", show_lines=False)
2168
+ table.add_column("framework", no_wrap=True)
2169
+ table.add_column("control", no_wrap=True)
2170
+ table.add_column("classification", no_wrap=True)
2171
+ rows = 0
2172
+ for pack in resolution.packs:
2173
+ if not pack.coverage:
2174
+ table.add_row(pack.framework, "-", "unknown")
2175
+ rows += 1
2176
+ continue
2177
+ for control, classification in sorted(pack.coverage.items()):
2178
+ table.add_row(pack.framework, control, classification)
2179
+ rows += 1
2180
+ if rows == 0:
2181
+ console.print("[dim]No coverage metadata available in selected packs.[/dim]")
2182
+ return
2183
+ console.print(table)
2184
+ return
2185
+
2186
+ # explain
2187
+ rule_id = (subject or "").strip()
2188
+ if not rule_id:
2189
+ console.print("[red]Rule id is required for `refactor compliance explain`.[/red]")
2190
+ raise typer.Exit(code=2)
2191
+ needle = rule_id.lower()
2192
+ matches = []
2193
+ for pack in resolution.packs:
2194
+ for rule in pack.rules:
2195
+ if rule.rule_id.lower() == needle or needle in rule.rule_id.lower():
2196
+ matches.append((pack, rule))
2197
+ if not matches:
2198
+ console.print(f"[yellow]No compliance rule found:[/yellow] {rule_id}")
2199
+ raise typer.Exit(code=1)
2200
+ for pack, rule in matches:
2201
+ console.print(
2202
+ f"[bold]{rule.rule_id}[/bold] "
2203
+ f"[dim](pack={pack.pack_id}@{pack.version}, framework={pack.framework})[/dim]"
2204
+ )
2205
+ if rule.source_standard:
2206
+ console.print(f" standard: {rule.source_standard}")
2207
+ if rule.control_objective:
2208
+ console.print(f" objective: {rule.control_objective}")
2209
+ console.print(f" operation_intent: {rule.operation_intent}")
2210
+ console.print(f" base_severity: {rule.base_severity}")
2211
+ console.print(f" priority: {rule.priority}")
2212
+
2213
+
1821
2214
  def _format_operation_scope(rr) -> str:
1822
2215
  scope = getattr(rr, "scope", None)
1823
2216
  if scope is None:
@@ -1848,6 +2241,131 @@ def _format_operation_scope(rr) -> str:
1848
2241
  return "-"
1849
2242
 
1850
2243
 
2244
+ def _replace_requests_with_framework_guardrails(
2245
+ *,
2246
+ project_root: Path,
2247
+ findings: list[Finding],
2248
+ run_id: str,
2249
+ compliance_frameworks: tuple[str, ...] = (),
2250
+ compliance_level: str = "standard",
2251
+ compliance_rollout: str = "default",
2252
+ compliance_canary_percent: int = 20,
2253
+ compliance_resolution: CompliancePackResolution | None = None,
2254
+ artifact: RunArtifact | None = None,
2255
+ ) -> list:
2256
+ clear_requests(project_root)
2257
+ requests, suppressions, warnings = create_requests(
2258
+ project_root,
2259
+ findings,
2260
+ run_id=run_id,
2261
+ compliance_frameworks=compliance_frameworks,
2262
+ compliance_level=compliance_level,
2263
+ compliance_rollout=compliance_rollout,
2264
+ compliance_canary_percent=compliance_canary_percent,
2265
+ return_diagnostics=True,
2266
+ )
2267
+ _report_framework_suppressions(suppressions, artifact=artifact)
2268
+ _report_framework_warnings(warnings, artifact=artifact)
2269
+ if artifact is not None and compliance_frameworks:
2270
+ compliance_suppressed = [
2271
+ item for item in suppressions if str(item.reason_code).startswith("RR_COMPLIANCE_")
2272
+ ]
2273
+ compliance_warnings = [
2274
+ item for item in warnings if str(item.reason_code).startswith("RR_COMPLIANCE_")
2275
+ ]
2276
+ artifact.compliance_summary = {
2277
+ "level": compliance_level,
2278
+ "frameworks": list(compliance_frameworks),
2279
+ "rollout": compliance_rollout,
2280
+ "canary_percent": int(compliance_canary_percent),
2281
+ "pack_versions": (
2282
+ compliance_resolution.pack_versions if compliance_resolution is not None else {}
2283
+ ),
2284
+ "projected_count": len(compliance_suppressed) + len(compliance_warnings),
2285
+ "blocked_count": len(compliance_suppressed),
2286
+ "warned_count": len(compliance_warnings),
2287
+ "rules_triggered": sorted(
2288
+ {
2289
+ item.rule_id
2290
+ for item in [*compliance_suppressed, *compliance_warnings]
2291
+ if str(item.rule_id).strip()
2292
+ }
2293
+ ),
2294
+ }
2295
+ return requests
2296
+
2297
+
2298
+ def _apply_compliance_gate_to_artifact(artifact: RunArtifact) -> None:
2299
+ """Promote compliance blocking decisions into gate outcome (R18 Phase 7)."""
2300
+ summary = dict(getattr(artifact, "compliance_summary", {}) or {})
2301
+ decision = evaluate_compliance_gate(
2302
+ summary,
2303
+ base_gate_status=str(getattr(artifact.gate, "status", "unknown") or "unknown"),
2304
+ )
2305
+ if decision.status == "blocked":
2306
+ artifact.gate.status = "blocked"
2307
+ reasons = list((artifact.gate.blocking_reasons or {}).get("reasons", []))
2308
+ for reason in decision.reasons:
2309
+ if reason not in reasons:
2310
+ reasons.append(reason)
2311
+ artifact.gate.blocking_reasons["reasons"] = reasons
2312
+
2313
+
2314
+ def _report_framework_suppressions(
2315
+ suppressions: list[RequestSuppression], *, artifact: RunArtifact | None = None
2316
+ ) -> None:
2317
+ if not suppressions:
2318
+ return
2319
+ feasibility = sum(1 for s in suppressions if s.rule_id == "operation_feasibility")
2320
+ compliance = sum(
2321
+ 1 for s in suppressions if s.reason_code.startswith("RR_COMPLIANCE_")
2322
+ )
2323
+ framework = len(suppressions) - feasibility - compliance
2324
+ parts: list[str] = []
2325
+ if framework:
2326
+ parts.append(f"{framework} due to framework invariants")
2327
+ if compliance:
2328
+ parts.append(f"{compliance} due to compliance controls")
2329
+ if feasibility:
2330
+ parts.append(f"{feasibility} not executable as written")
2331
+ detail = "; ".join(parts) if parts else "review-stage checks"
2332
+ console.print(
2333
+ f"[yellow]warning:[/yellow] suppressed {len(suppressions)} finding(s) ({detail})."
2334
+ )
2335
+ for item in suppressions:
2336
+ console.print(
2337
+ f"[yellow]-[/yellow] {item.location}: {item.message} "
2338
+ f"[dim](framework={item.framework or 'unknown'}, rule={item.rule_id or 'unspecified'})[/dim]"
2339
+ )
2340
+ if artifact is not None:
2341
+ artifact.notes.append(item.note())
2342
+
2343
+
2344
+ def _report_framework_warnings(
2345
+ warnings: list[RequestWarning], *, artifact: RunArtifact | None = None
2346
+ ) -> None:
2347
+ if not warnings:
2348
+ return
2349
+ compliance = sum(1 for item in warnings if item.reason_code.startswith("RR_COMPLIANCE_"))
2350
+ framework = len(warnings) - compliance
2351
+ detail_parts: list[str] = []
2352
+ if framework:
2353
+ detail_parts.append(f"{framework} framework")
2354
+ if compliance:
2355
+ detail_parts.append(f"{compliance} compliance")
2356
+ detail = ", ".join(detail_parts) if detail_parts else "review"
2357
+ console.print(
2358
+ f"[yellow]warning:[/yellow] flagged {len(warnings)} finding(s) with caution ({detail})."
2359
+ )
2360
+ for item in warnings:
2361
+ console.print(
2362
+ f"[yellow]-[/yellow] {item.location}: {item.message} "
2363
+ f"[dim](framework={item.framework or 'unknown'}, rule={item.rule_id or 'unspecified'})[/dim]"
2364
+ )
2365
+ if artifact is not None:
2366
+ artifact.notes.append(item.note())
2367
+
2368
+
1851
2369
  def apply(
1852
2370
  force: bool = typer.Option(False, "--force", help="Apply even if the quality gate blocks."),
1853
2371
  ) -> None:
@@ -2105,6 +2623,11 @@ def gc(
2105
2623
 
2106
2624
  def check(
2107
2625
  strict: bool = typer.Option(False, "--strict", help="Exit non-zero if issues are found (for CI)."),
2626
+ compliance_gate: bool = typer.Option(
2627
+ False,
2628
+ "--compliance-gate",
2629
+ help="Also enforce latest run compliance gate status (for CI/PR pipelines).",
2630
+ ),
2108
2631
  ) -> None:
2109
2632
  """Check the constitution for committed secrets (developer/provider keys)."""
2110
2633
  files = find_project_files()
@@ -2115,6 +2638,25 @@ def check(
2115
2638
  constitution = load_constitution(consti_path)
2116
2639
  config = load_config(config_path)
2117
2640
  findings = raw_secret_matches(constitution) + raw_secret_matches_config(config)
2641
+ compliance_failed = False
2642
+ if compliance_gate:
2643
+ run_dir = head_run_dir(consti_path.parent)
2644
+ if run_dir is None:
2645
+ console.print("[yellow]Compliance gate check skipped:[/yellow] no runs yet.")
2646
+ else:
2647
+ report = read_report(run_dir) or {}
2648
+ decision = evaluate_compliance_gate(
2649
+ report.get("compliance_summary", {}),
2650
+ base_gate_status=str((report.get("gate") or {}).get("status", "unknown")),
2651
+ )
2652
+ if decision.status == "blocked":
2653
+ compliance_failed = True
2654
+ console.print(
2655
+ "[red]Compliance gate blocked[/red]: "
2656
+ + ", ".join(decision.reasons or ("compliance_controls_blocked",))
2657
+ )
2658
+ else:
2659
+ console.print("[green]Compliance gate[/green]: passed")
2118
2660
  if findings:
2119
2661
  kinds = ", ".join(sorted({kind for kind, _ in findings}))
2120
2662
  console.print(
@@ -2122,6 +2664,8 @@ def check(
2122
2664
  f"{consti_path} or {config_path}. Replace secrets with `${{ENV_VAR}}` references."
2123
2665
  )
2124
2666
  raise typer.Exit(code=1 if strict else 0)
2667
+ if compliance_failed:
2668
+ raise typer.Exit(code=1 if strict else 0)
2125
2669
  console.print("[green]OK[/green]: no committed secret-like literals detected.")
2126
2670
 
2127
2671
 
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import typer
6
6
  from rich.console import Console
7
+ from rich.progress import BarColumn, DownloadColumn, Progress, TaskID, TextColumn, TimeRemainingColumn, TransferSpeedColumn
7
8
 
8
9
  from refactorai_cli.auth import AuthError, ensure_authenticated
9
10
  from refactorai_cli.control_plane import ensure_lease, heartbeat, read_cached_lease, resolve_policy
@@ -62,7 +63,23 @@ def update(
62
63
  f"[bold]resolved manifest[/bold]: version={manifest.runtime_version} "
63
64
  f"channel={channel}"
64
65
  )
65
- artifact = download_artifact(manifest.artifact_url)
66
+ progress_columns = (
67
+ TextColumn("[progress.description]{task.description}"),
68
+ BarColumn(),
69
+ DownloadColumn(),
70
+ TransferSpeedColumn(),
71
+ TimeRemainingColumn(),
72
+ )
73
+ with Progress(*progress_columns, console=console, transient=True) as progress:
74
+ task_id: TaskID = progress.add_task("Downloading runtime artifact", total=None)
75
+
76
+ def _on_progress(downloaded: int, total: int | None) -> None:
77
+ update_kwargs = {"completed": int(max(downloaded, 0))}
78
+ if total is not None and total > 0:
79
+ update_kwargs["total"] = total
80
+ progress.update(task_id, **update_kwargs)
81
+
82
+ artifact = download_artifact(manifest.artifact_url, progress_cb=_on_progress)
66
83
  console.print(f"[bold]downloaded artifact[/bold]: {len(artifact)} bytes")
67
84
  if dry_run:
68
85
  # Verification happens in activate_runtime; dry-run still checks
@@ -136,3 +136,8 @@ def doctor(ctx: typer.Context) -> None:
136
136
  def config(ctx: typer.Context) -> None:
137
137
  """Manage config via proprietary runtime."""
138
138
  _exec_runtime("config", list(ctx.args))
139
+
140
+
141
+ def compliance(ctx: typer.Context) -> None:
142
+ """Inspect compliance packs/coverage via proprietary runtime."""
143
+ _exec_runtime("compliance", list(ctx.args))
@@ -56,6 +56,7 @@ app.command(context_settings=_RUNTIME_CTX)(runtime_proxy_cmds.gc)
56
56
  app.command(context_settings=_RUNTIME_CTX)(runtime_proxy_cmds.check)
57
57
  app.command(context_settings=_RUNTIME_CTX)(runtime_proxy_cmds.doctor)
58
58
  app.command(context_settings=_RUNTIME_CTX)(runtime_proxy_cmds.config)
59
+ app.command(context_settings=_RUNTIME_CTX)(runtime_proxy_cmds.compliance)
59
60
 
60
61
  app.command()(setup_cmds.setup)
61
62
 
@@ -14,6 +14,7 @@ import platform
14
14
  from dataclasses import dataclass
15
15
  from datetime import datetime, timezone
16
16
  from pathlib import Path
17
+ from typing import Callable
17
18
  from urllib.parse import urlparse
18
19
 
19
20
  import httpx
@@ -153,17 +154,49 @@ def resolve_runtime_manifest(*, channel: str = "stable", timeout: float = 20.0)
153
154
  return manifest
154
155
 
155
156
 
156
- def download_artifact(url: str, *, timeout: float = 60.0) -> bytes:
157
+ def download_artifact(
158
+ url: str,
159
+ *,
160
+ timeout: float = 60.0,
161
+ progress_cb: Callable[[int, int | None], None] | None = None,
162
+ ) -> bytes:
157
163
  headers: dict[str, str] = {}
158
164
  if _should_send_runtime_auth(url):
159
165
  headers["Authorization"] = _runtime_auth_header()
166
+ resolved_timeout = max(float(timeout), 300.0)
167
+ if progress_cb is None:
168
+ try:
169
+ response = httpx.get(url, headers=headers, timeout=resolved_timeout)
170
+ except httpx.HTTPError as exc:
171
+ raise RuntimeError(f"Could not download runtime artifact: {exc}") from exc
172
+ if response.status_code >= 400:
173
+ raise RuntimeError(f"Runtime artifact download failed ({response.status_code})")
174
+ return response.content
160
175
  try:
161
- response = httpx.get(url, headers=headers, timeout=max(float(timeout), 300.0))
176
+ with httpx.stream("GET", url, headers=headers, timeout=resolved_timeout) as response:
177
+ if response.status_code >= 400:
178
+ raise RuntimeError(f"Runtime artifact download failed ({response.status_code})")
179
+ total: int | None = None
180
+ total_header = str(response.headers.get("Content-Length") or "").strip()
181
+ if total_header:
182
+ try:
183
+ parsed_total = int(total_header)
184
+ except ValueError:
185
+ parsed_total = -1
186
+ if parsed_total > 0:
187
+ total = parsed_total
188
+ downloaded = 0
189
+ chunks: list[bytes] = []
190
+ progress_cb(downloaded, total)
191
+ for chunk in response.iter_bytes(chunk_size=64 * 1024):
192
+ if not chunk:
193
+ continue
194
+ chunks.append(chunk)
195
+ downloaded += len(chunk)
196
+ progress_cb(downloaded, total)
162
197
  except httpx.HTTPError as exc:
163
198
  raise RuntimeError(f"Could not download runtime artifact: {exc}") from exc
164
- if response.status_code >= 400:
165
- raise RuntimeError(f"Runtime artifact download failed ({response.status_code})")
166
- return response.content
199
+ return b"".join(chunks)
167
200
 
168
201
 
169
202
  def _should_send_runtime_auth(url: str) -> bool:
@@ -10,6 +10,9 @@ from pathlib import Path
10
10
  from inspect import signature
11
11
  from typing import Callable
12
12
 
13
+ from rich.console import Console
14
+ from rich.progress import BarColumn, DownloadColumn, Progress, TaskID, TextColumn, TimeRemainingColumn, TransferSpeedColumn
15
+
13
16
  from refactorai_cli.local_engine_runtime import (
14
17
  DEFAULT_ENGINE_CONTAINER,
15
18
  DEFAULT_ENGINE_IMAGE,
@@ -39,6 +42,7 @@ STAGE_NAMES = {
39
42
  "S5": "backend_bootstrap",
40
43
  "S6": "validate",
41
44
  }
45
+ _setup_console = Console()
42
46
 
43
47
 
44
48
  class SetupError(RuntimeError):
@@ -180,7 +184,23 @@ def _stage_s3_runtime(ask_approval: Callable[[str], bool], emit: Callable[[str],
180
184
  raise SetupError("Runtime installation was not approved.")
181
185
  if emit:
182
186
  emit(f"Downloading runtime artifact {manifest.runtime_version}...")
183
- artifact = download_artifact(manifest.artifact_url)
187
+ progress_columns = (
188
+ TextColumn("[progress.description]{task.description}"),
189
+ BarColumn(),
190
+ DownloadColumn(),
191
+ TransferSpeedColumn(),
192
+ TimeRemainingColumn(),
193
+ )
194
+ with Progress(*progress_columns, console=_setup_console, transient=True) as progress:
195
+ task_id: TaskID = progress.add_task("Downloading runtime artifact", total=None)
196
+
197
+ def _on_progress(downloaded: int, total: int | None) -> None:
198
+ update_kwargs = {"completed": int(max(downloaded, 0))}
199
+ if total is not None and total > 0:
200
+ update_kwargs["total"] = total
201
+ progress.update(task_id, **update_kwargs)
202
+
203
+ artifact = download_artifact(manifest.artifact_url, progress_cb=_on_progress)
184
204
  if emit:
185
205
  emit("Verifying and activating runtime artifact...")
186
206
  path = activate_runtime(manifest, artifact, channel="stable")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: refactorai-cli
3
- Version: 0.2.15
3
+ Version: 0.2.17
4
4
  Summary: Local-first CLI for the refactor platform
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown