refactorai-cli 0.2.16__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.16 → refactorai_cli-0.2.17}/PKG-INFO +1 -1
  2. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/pyproject.toml +1 -1
  3. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/__init__.py +1 -1
  4. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/commands/run_cmds.py +485 -2
  5. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/commands/runtime_proxy_cmds.py +5 -0
  6. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/main.py +1 -0
  7. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/setup_flow.py +21 -1
  8. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli.egg-info/PKG-INFO +1 -1
  9. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/README.md +0 -0
  10. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/auth.py +0 -0
  11. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/client.py +0 -0
  12. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/commands/__init__.py +0 -0
  13. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/commands/auth_cmds.py +0 -0
  14. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/commands/engine_cmds.py +0 -0
  15. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/commands/model_cmds.py +0 -0
  16. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/commands/rules_cmds.py +0 -0
  17. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/commands/runtime_cmds.py +0 -0
  18. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/commands/setup_cmds.py +0 -0
  19. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/control_plane.py +0 -0
  20. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/credentials.py +0 -0
  21. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/local_constitution.py +0 -0
  22. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/local_engine_runtime.py +0 -0
  23. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/local_paths.py +0 -0
  24. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/model_policy.py +0 -0
  25. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/runtime_manager.py +0 -0
  26. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli/settings.py +0 -0
  27. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli.egg-info/SOURCES.txt +0 -0
  28. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli.egg-info/dependency_links.txt +0 -0
  29. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli.egg-info/entry_points.txt +0 -0
  30. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli.egg-info/requires.txt +0 -0
  31. {refactorai_cli-0.2.16 → refactorai_cli-0.2.17}/refactorai_cli.egg-info/top_level.txt +0 -0
  32. {refactorai_cli-0.2.16 → 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.16
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.16"
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.16"
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,
@@ -661,6 +665,35 @@ def _warn_raw_key(constitution, config) -> None:
661
665
  )
662
666
 
663
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
+
664
697
  def init(
665
698
  force: bool = typer.Option(
666
699
  False,
@@ -1481,10 +1514,61 @@ def review(
1481
1514
  "--force",
1482
1515
  help="Run review even if unapplied patches exist from a prior code run.",
1483
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
+ ),
1484
1537
  ) -> None:
1485
1538
  """Run review (findings only) and store the result. Requires a valid key."""
1486
1539
  _require_auth()
1487
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}")
1488
1572
  register_project(project_root, constitution_hash=constitution.content_hash)
1489
1573
  _maybe_build_graph(project_root, project_config)
1490
1574
  sandbox_ctx = None
@@ -1562,8 +1646,34 @@ def review(
1562
1646
  project_root=project_root,
1563
1647
  findings=artifact.findings,
1564
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,
1565
1654
  artifact=artifact,
1566
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}")
1567
1677
  if requests:
1568
1678
  console.print(
1569
1679
  f"\n[bold]Created {len(requests)} refactor request(s).[/bold] "
@@ -1608,6 +1718,26 @@ def code(
1608
1718
  "--equivalence",
1609
1719
  help="Equivalence policy: strict | balanced.",
1610
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
+ ),
1611
1741
  ) -> None:
1612
1742
  """Work through refactor requests one by one, applying & verifying each.
1613
1743
 
@@ -1616,6 +1746,37 @@ def code(
1616
1746
  """
1617
1747
  _require_auth()
1618
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}")
1619
1780
  register_project(project_root, constitution_hash=constitution.content_hash)
1620
1781
  _maybe_build_graph(project_root, project_config)
1621
1782
  sandbox_ctx = None
@@ -1676,8 +1837,34 @@ def code(
1676
1837
  project_root=project_root,
1677
1838
  findings=review_artifact.findings,
1678
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,
1679
1845
  artifact=review_artifact,
1680
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")
1681
1868
  console.print(f"[bold]Created {len(requests)} refactor request(s).[/bold]")
1682
1869
 
1683
1870
  if not requests:
@@ -1826,6 +2013,204 @@ def requests(
1826
2013
  console.print(table)
1827
2014
 
1828
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
+
1829
2214
  def _format_operation_scope(rr) -> str:
1830
2215
  scope = getattr(rr, "scope", None)
1831
2216
  if scope is None:
@@ -1861,6 +2246,11 @@ def _replace_requests_with_framework_guardrails(
1861
2246
  project_root: Path,
1862
2247
  findings: list[Finding],
1863
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,
1864
2254
  artifact: RunArtifact | None = None,
1865
2255
  ) -> list:
1866
2256
  clear_requests(project_root)
@@ -1868,20 +2258,79 @@ def _replace_requests_with_framework_guardrails(
1868
2258
  project_root,
1869
2259
  findings,
1870
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,
1871
2265
  return_diagnostics=True,
1872
2266
  )
1873
2267
  _report_framework_suppressions(suppressions, artifact=artifact)
1874
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
+ }
1875
2295
  return requests
1876
2296
 
1877
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
+
1878
2314
  def _report_framework_suppressions(
1879
2315
  suppressions: list[RequestSuppression], *, artifact: RunArtifact | None = None
1880
2316
  ) -> None:
1881
2317
  if not suppressions:
1882
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"
1883
2332
  console.print(
1884
- f"[yellow]warning:[/yellow] suppressed {len(suppressions)} finding(s) due to framework invariants."
2333
+ f"[yellow]warning:[/yellow] suppressed {len(suppressions)} finding(s) ({detail})."
1885
2334
  )
1886
2335
  for item in suppressions:
1887
2336
  console.print(
@@ -1897,8 +2346,16 @@ def _report_framework_warnings(
1897
2346
  ) -> None:
1898
2347
  if not warnings:
1899
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"
1900
2357
  console.print(
1901
- f"[yellow]warning:[/yellow] flagged {len(warnings)} finding(s) with framework caution."
2358
+ f"[yellow]warning:[/yellow] flagged {len(warnings)} finding(s) with caution ({detail})."
1902
2359
  )
1903
2360
  for item in warnings:
1904
2361
  console.print(
@@ -2166,6 +2623,11 @@ def gc(
2166
2623
 
2167
2624
  def check(
2168
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
+ ),
2169
2631
  ) -> None:
2170
2632
  """Check the constitution for committed secrets (developer/provider keys)."""
2171
2633
  files = find_project_files()
@@ -2176,6 +2638,25 @@ def check(
2176
2638
  constitution = load_constitution(consti_path)
2177
2639
  config = load_config(config_path)
2178
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")
2179
2660
  if findings:
2180
2661
  kinds = ", ".join(sorted({kind for kind, _ in findings}))
2181
2662
  console.print(
@@ -2183,6 +2664,8 @@ def check(
2183
2664
  f"{consti_path} or {config_path}. Replace secrets with `${{ENV_VAR}}` references."
2184
2665
  )
2185
2666
  raise typer.Exit(code=1 if strict else 0)
2667
+ if compliance_failed:
2668
+ raise typer.Exit(code=1 if strict else 0)
2186
2669
  console.print("[green]OK[/green]: no committed secret-like literals detected.")
2187
2670
 
2188
2671
 
@@ -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
 
@@ -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.16
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