refactorai-cli 0.2.16__tar.gz → 0.2.19__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.19}/PKG-INFO +1 -1
  2. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/pyproject.toml +1 -1
  3. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/__init__.py +1 -1
  4. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/commands/run_cmds.py +526 -6
  5. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/commands/runtime_proxy_cmds.py +5 -0
  6. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/main.py +1 -0
  7. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/setup_flow.py +21 -1
  8. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli.egg-info/PKG-INFO +1 -1
  9. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/README.md +0 -0
  10. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/auth.py +0 -0
  11. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/client.py +0 -0
  12. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/commands/__init__.py +0 -0
  13. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/commands/auth_cmds.py +0 -0
  14. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/commands/engine_cmds.py +0 -0
  15. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/commands/model_cmds.py +0 -0
  16. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/commands/rules_cmds.py +0 -0
  17. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/commands/runtime_cmds.py +0 -0
  18. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/commands/setup_cmds.py +0 -0
  19. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/control_plane.py +0 -0
  20. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/credentials.py +0 -0
  21. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/local_constitution.py +0 -0
  22. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/local_engine_runtime.py +0 -0
  23. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/local_paths.py +0 -0
  24. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/model_policy.py +0 -0
  25. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/runtime_manager.py +0 -0
  26. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli/settings.py +0 -0
  27. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli.egg-info/SOURCES.txt +0 -0
  28. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli.egg-info/dependency_links.txt +0 -0
  29. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli.egg-info/entry_points.txt +0 -0
  30. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli.egg-info/requires.txt +0 -0
  31. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/refactorai_cli.egg-info/top_level.txt +0 -0
  32. {refactorai_cli-0.2.16 → refactorai_cli-0.2.19}/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.19
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.19"
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.19"
@@ -35,6 +35,13 @@ 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 (
39
+ ComplianceSelectionError,
40
+ resolve_compliance_selection,
41
+ )
42
+ from refactor_core.compliance.loader import CompliancePackError, resolve_compliance_packs
43
+ from refactor_core.compliance.models import CompliancePackResolution
44
+ from refactor_core.compliance.evidence import build_compliance_evidence_bundle, evaluate_compliance_gate
38
45
  from refactor_core.apply import (
39
46
  ApplyError,
40
47
  apply_changes,
@@ -180,6 +187,25 @@ _REQUIRED_ENV_BY_PROVIDER: dict[str, list[object]] = {
180
187
  }
181
188
 
182
189
 
190
+ def _sanitize_runtime_error_message(message: object) -> str:
191
+ """Normalize provider/runtime errors to runtime-artifact-friendly guidance."""
192
+ text = str(message or "").strip()
193
+ if not text:
194
+ return "unknown"
195
+ install_hint = re.search(
196
+ r"Install with:\s*pip install ['\"]refactor-core(?:\[[^'\"]+\])?['\"]",
197
+ text,
198
+ flags=re.IGNORECASE,
199
+ )
200
+ if install_hint:
201
+ return (
202
+ "Bedrock credentials are not configured for runtime execution. "
203
+ "Set AWS_BEARER_TOKEN_BEDROCK (for example: `set -a; source .env; set +a`) "
204
+ "or run with AWS SDK credentials in an environment that includes boto3."
205
+ )
206
+ return text
207
+
208
+
183
209
  def _agent_progress_logger(
184
210
  stage: str, *, status=None, verbose_steps: bool = True
185
211
  ) -> Callable[[dict], None]:
@@ -298,16 +324,18 @@ def _agent_progress_logger(
298
324
  f"[dim]{stage}: step {event.get('step', '?')} result -> {summary}[/dim]"
299
325
  )
300
326
  elif kind == "tool_error":
327
+ error_text = _sanitize_runtime_error_message(event.get("error", "unknown"))
301
328
  console.print(
302
329
  f"[yellow]{stage}: step {event.get('step', '?')} tool `{event.get('tool', '?')}` "
303
- f"error: {event.get('error', 'unknown')}[/yellow]"
330
+ f"error: {error_text}[/yellow]"
304
331
  )
305
332
  elif kind == "invalid_response":
306
333
  console.print(f"[yellow]{stage}: step {event.get('step', '?')} returned invalid JSON tool call.[/yellow]")
307
334
  elif kind == "transport_error":
335
+ error_text = _sanitize_runtime_error_message(event.get("error", "unknown"))
308
336
  console.print(
309
337
  f"[yellow]{stage}: model transport error at step {event.get('step', '?')}: "
310
- f"{event.get('error', 'unknown')}[/yellow]"
338
+ f"{error_text}[/yellow]"
311
339
  )
312
340
  elif kind == "finish":
313
341
  _set_status(
@@ -661,6 +689,35 @@ def _warn_raw_key(constitution, config) -> None:
661
689
  )
662
690
 
663
691
 
692
+ def _resolve_compliance_context(
693
+ *,
694
+ project_config,
695
+ cli_frameworks: list[str] | None = None,
696
+ cli_level: str | None = None,
697
+ cli_rollout: str | None = None,
698
+ cli_canary_percent: int | None = None,
699
+ ) -> tuple[tuple[str, ...], str, CompliancePackResolution, str, int]:
700
+ """Resolve compliance selection + managed pack set for this run."""
701
+ selection = resolve_compliance_selection(
702
+ dict(getattr(project_config, "settings", {}) or {}),
703
+ cli_frameworks=cli_frameworks,
704
+ cli_level=cli_level,
705
+ cli_rollout=cli_rollout,
706
+ cli_canary_percent=cli_canary_percent,
707
+ )
708
+ resolution = resolve_compliance_packs(
709
+ selection.frameworks,
710
+ level=selection.level,
711
+ )
712
+ return (
713
+ selection.frameworks,
714
+ selection.level,
715
+ resolution,
716
+ selection.rollout,
717
+ selection.canary_percent,
718
+ )
719
+
720
+
664
721
  def init(
665
722
  force: bool = typer.Option(
666
723
  False,
@@ -1481,10 +1538,64 @@ def review(
1481
1538
  "--force",
1482
1539
  help="Run review even if unapplied patches exist from a prior code run.",
1483
1540
  ),
1541
+ compliance_framework: list[str] | None = typer.Option(
1542
+ None,
1543
+ "--compliance-framework",
1544
+ help="Compliance framework to enable for this run. Repeat for multiple values.",
1545
+ ),
1546
+ compliance_level: str | None = typer.Option(
1547
+ None,
1548
+ "--compliance-level",
1549
+ help="Compliance level override: off | advisory | standard | strict.",
1550
+ ),
1551
+ compliance_rollout: str | None = typer.Option(
1552
+ None,
1553
+ "--compliance-rollout",
1554
+ help="Compliance rollout mode: default | staged | canary | off | ga.",
1555
+ ),
1556
+ compliance_canary_percent: int | None = typer.Option(
1557
+ None,
1558
+ "--compliance-canary-percent",
1559
+ help="Compliance canary percentage (0-100) when rollout=canary.",
1560
+ ),
1484
1561
  ) -> None:
1485
1562
  """Run review (findings only) and store the result. Requires a valid key."""
1486
1563
  _require_auth()
1487
1564
  project_root, constitution, project_config = _load_project()
1565
+ try:
1566
+ (
1567
+ compliance_frameworks,
1568
+ resolved_compliance_level,
1569
+ compliance_resolution,
1570
+ compliance_rollout_mode,
1571
+ compliance_canary_pct,
1572
+ ) = (
1573
+ _resolve_compliance_context(
1574
+ project_config=project_config,
1575
+ cli_frameworks=compliance_framework,
1576
+ cli_level=compliance_level,
1577
+ cli_rollout=compliance_rollout,
1578
+ cli_canary_percent=compliance_canary_percent,
1579
+ )
1580
+ )
1581
+ except ComplianceSelectionError as exc:
1582
+ console.print(f"[red]review: invalid compliance selection:[/red] {exc}")
1583
+ raise typer.Exit(code=2) from exc
1584
+ except CompliancePackError as exc:
1585
+ console.print(f"[red]review: compliance pack resolution failed:[/red] {exc}")
1586
+ raise typer.Exit(code=1) from exc
1587
+ if compliance_frameworks:
1588
+ pack_label = ", ".join(
1589
+ f"{pack.pack_id}@{pack.version}" for pack in compliance_resolution.packs
1590
+ ) or "(none loaded)"
1591
+ console.print(
1592
+ "[dim]review: compliance selection "
1593
+ f"frameworks={list(compliance_frameworks)} level={resolved_compliance_level} "
1594
+ f"rollout={compliance_rollout_mode} canary_percent={compliance_canary_pct} "
1595
+ f"packs={pack_label}[/dim]"
1596
+ )
1597
+ for warning in compliance_resolution.warnings:
1598
+ console.print(f"[yellow]review: compliance warning:[/yellow] {warning}")
1488
1599
  register_project(project_root, constitution_hash=constitution.content_hash)
1489
1600
  _maybe_build_graph(project_root, project_config)
1490
1601
  sandbox_ctx = None
@@ -1546,7 +1657,7 @@ def review(
1546
1657
  "Review continued; address sandbox install/toolchain incompatibilities for best results."
1547
1658
  )
1548
1659
  except Exception as exc: # provider/transport errors should not crash the CLI
1549
- console.print(f"[red]Review failed:[/red] {exc}")
1660
+ console.print(f"[red]Review failed:[/red] {_sanitize_runtime_error_message(exc)}")
1550
1661
  raise typer.Exit(code=1) from exc
1551
1662
  finally:
1552
1663
  _teardown_sandbox_context(project_root, sandbox_ctx)
@@ -1562,8 +1673,34 @@ def review(
1562
1673
  project_root=project_root,
1563
1674
  findings=artifact.findings,
1564
1675
  run_id=artifact.run_id,
1676
+ compliance_frameworks=compliance_frameworks,
1677
+ compliance_level=resolved_compliance_level,
1678
+ compliance_rollout=compliance_rollout_mode,
1679
+ compliance_canary_percent=compliance_canary_pct,
1680
+ compliance_resolution=compliance_resolution,
1565
1681
  artifact=artifact,
1566
1682
  )
1683
+ _apply_compliance_gate_to_artifact(artifact)
1684
+ run_store.write_artifact(artifact, target=target, status="completed")
1685
+ if compliance_frameworks:
1686
+ artifact.notes.append(
1687
+ "Compliance selection: frameworks="
1688
+ + ",".join(compliance_frameworks)
1689
+ + (
1690
+ f" level={resolved_compliance_level} rollout={compliance_rollout_mode} "
1691
+ f"canary_percent={compliance_canary_pct}"
1692
+ )
1693
+ )
1694
+ if compliance_resolution.packs:
1695
+ artifact.notes.append(
1696
+ "Compliance packs: "
1697
+ + ", ".join(
1698
+ f"{pack.pack_id}@{pack.version}"
1699
+ for pack in compliance_resolution.packs
1700
+ )
1701
+ )
1702
+ for warning in compliance_resolution.warnings:
1703
+ artifact.notes.append(f"Compliance pack warning: {warning}")
1567
1704
  if requests:
1568
1705
  console.print(
1569
1706
  f"\n[bold]Created {len(requests)} refactor request(s).[/bold] "
@@ -1608,6 +1745,26 @@ def code(
1608
1745
  "--equivalence",
1609
1746
  help="Equivalence policy: strict | balanced.",
1610
1747
  ),
1748
+ compliance_framework: list[str] | None = typer.Option(
1749
+ None,
1750
+ "--compliance-framework",
1751
+ help="Compliance framework to enable for this run. Repeat for multiple values.",
1752
+ ),
1753
+ compliance_level: str | None = typer.Option(
1754
+ None,
1755
+ "--compliance-level",
1756
+ help="Compliance level override: off | advisory | standard | strict.",
1757
+ ),
1758
+ compliance_rollout: str | None = typer.Option(
1759
+ None,
1760
+ "--compliance-rollout",
1761
+ help="Compliance rollout mode: default | staged | canary | off | ga.",
1762
+ ),
1763
+ compliance_canary_percent: int | None = typer.Option(
1764
+ None,
1765
+ "--compliance-canary-percent",
1766
+ help="Compliance canary percentage (0-100) when rollout=canary.",
1767
+ ),
1611
1768
  ) -> None:
1612
1769
  """Work through refactor requests one by one, applying & verifying each.
1613
1770
 
@@ -1616,6 +1773,40 @@ def code(
1616
1773
  """
1617
1774
  _require_auth()
1618
1775
  project_root, constitution, project_config = _load_project()
1776
+ try:
1777
+ (
1778
+ compliance_frameworks,
1779
+ resolved_compliance_level,
1780
+ compliance_resolution,
1781
+ compliance_rollout_mode,
1782
+ compliance_canary_pct,
1783
+ ) = (
1784
+ _resolve_compliance_context(
1785
+ project_config=project_config,
1786
+ cli_frameworks=compliance_framework,
1787
+ cli_level=compliance_level,
1788
+ cli_rollout=compliance_rollout,
1789
+ cli_canary_percent=compliance_canary_percent,
1790
+ )
1791
+ )
1792
+ except ComplianceSelectionError as exc:
1793
+ console.print(f"[red]code: invalid compliance selection:[/red] {exc}")
1794
+ raise typer.Exit(code=2) from exc
1795
+ except CompliancePackError as exc:
1796
+ console.print(f"[red]code: compliance pack resolution failed:[/red] {exc}")
1797
+ raise typer.Exit(code=1) from exc
1798
+ if compliance_frameworks:
1799
+ pack_label = ", ".join(
1800
+ f"{pack.pack_id}@{pack.version}" for pack in compliance_resolution.packs
1801
+ ) or "(none loaded)"
1802
+ console.print(
1803
+ "[dim]code: compliance selection "
1804
+ f"frameworks={list(compliance_frameworks)} level={resolved_compliance_level} "
1805
+ f"rollout={compliance_rollout_mode} canary_percent={compliance_canary_pct} "
1806
+ f"packs={pack_label}[/dim]"
1807
+ )
1808
+ for warning in compliance_resolution.warnings:
1809
+ console.print(f"[yellow]code: compliance warning:[/yellow] {warning}")
1619
1810
  register_project(project_root, constitution_hash=constitution.content_hash)
1620
1811
  _maybe_build_graph(project_root, project_config)
1621
1812
  sandbox_ctx = None
@@ -1676,8 +1867,34 @@ def code(
1676
1867
  project_root=project_root,
1677
1868
  findings=review_artifact.findings,
1678
1869
  run_id=review_artifact.run_id,
1870
+ compliance_frameworks=compliance_frameworks,
1871
+ compliance_level=resolved_compliance_level,
1872
+ compliance_rollout=compliance_rollout_mode,
1873
+ compliance_canary_percent=compliance_canary_pct,
1874
+ compliance_resolution=compliance_resolution,
1679
1875
  artifact=review_artifact,
1680
1876
  )
1877
+ _apply_compliance_gate_to_artifact(review_artifact)
1878
+ if compliance_frameworks:
1879
+ review_artifact.notes.append(
1880
+ "Compliance selection: frameworks="
1881
+ + ",".join(compliance_frameworks)
1882
+ + (
1883
+ f" level={resolved_compliance_level} rollout={compliance_rollout_mode} "
1884
+ f"canary_percent={compliance_canary_pct}"
1885
+ )
1886
+ )
1887
+ if compliance_resolution.packs:
1888
+ review_artifact.notes.append(
1889
+ "Compliance packs: "
1890
+ + ", ".join(
1891
+ f"{pack.pack_id}@{pack.version}"
1892
+ for pack in compliance_resolution.packs
1893
+ )
1894
+ )
1895
+ for warning in compliance_resolution.warnings:
1896
+ review_artifact.notes.append(f"Compliance pack warning: {warning}")
1897
+ review_store.write_artifact(review_artifact, target=target, status="completed")
1681
1898
  console.print(f"[bold]Created {len(requests)} refactor request(s).[/bold]")
1682
1899
 
1683
1900
  if not requests:
@@ -1710,7 +1927,7 @@ def code(
1710
1927
  equivalence_mode=equivalence,
1711
1928
  )
1712
1929
  except Exception as exc:
1713
- console.print(f"[red]Refactor failed:[/red] {exc}")
1930
+ console.print(f"[red]Refactor failed:[/red] {_sanitize_runtime_error_message(exc)}")
1714
1931
  raise typer.Exit(code=1) from exc
1715
1932
  finally:
1716
1933
  _teardown_sandbox_context(project_root, sandbox_ctx)
@@ -1826,6 +2043,211 @@ def requests(
1826
2043
  console.print(table)
1827
2044
 
1828
2045
 
2046
+ def compliance(
2047
+ action: str = typer.Argument(
2048
+ "show",
2049
+ help="Compliance view action: status | show | coverage | explain | export",
2050
+ ),
2051
+ subject: str = typer.Argument(
2052
+ "",
2053
+ help="Rule ID when action=explain.",
2054
+ ),
2055
+ compliance_framework: list[str] | None = typer.Option(
2056
+ None,
2057
+ "--compliance-framework",
2058
+ help="Compliance framework selection override. Repeat for multiple values.",
2059
+ ),
2060
+ compliance_level: str | None = typer.Option(
2061
+ None,
2062
+ "--compliance-level",
2063
+ help="Compliance level override: off | advisory | standard | strict.",
2064
+ ),
2065
+ compliance_rollout: str | None = typer.Option(
2066
+ None,
2067
+ "--compliance-rollout",
2068
+ help="Compliance rollout mode override: default | staged | canary | off | ga.",
2069
+ ),
2070
+ compliance_canary_percent: int | None = typer.Option(
2071
+ None,
2072
+ "--compliance-canary-percent",
2073
+ help="Compliance canary percentage override when rollout=canary.",
2074
+ ),
2075
+ output: str = typer.Option(
2076
+ "",
2077
+ "--output",
2078
+ help="Output file path for action=export (defaults to latest run directory).",
2079
+ ),
2080
+ ) -> None:
2081
+ """Show/export compliance packs, coverage, and rule details."""
2082
+ _require_auth()
2083
+ _project_root, _constitution, project_config = _load_project()
2084
+ try:
2085
+ frameworks, level, resolution, rollout_mode, canary_percent = _resolve_compliance_context(
2086
+ project_config=project_config,
2087
+ cli_frameworks=compliance_framework,
2088
+ cli_level=compliance_level,
2089
+ cli_rollout=compliance_rollout,
2090
+ cli_canary_percent=compliance_canary_percent,
2091
+ )
2092
+ except ComplianceSelectionError as exc:
2093
+ console.print(f"[red]compliance: invalid selection:[/red] {exc}")
2094
+ raise typer.Exit(code=2) from exc
2095
+ except CompliancePackError as exc:
2096
+ console.print(f"[red]compliance: pack resolution failed:[/red] {exc}")
2097
+ raise typer.Exit(code=1) from exc
2098
+ mode = str(action or "show").strip().lower()
2099
+ if mode not in {"status", "show", "coverage", "explain", "export"}:
2100
+ console.print(f"[red]Unknown compliance action:[/red] {action}")
2101
+ raise typer.Exit(code=2)
2102
+ if not frameworks:
2103
+ console.print(
2104
+ "[dim]No compliance frameworks selected. "
2105
+ "Set `compliance.frameworks` in refactor.config or pass --compliance-framework.[/dim]"
2106
+ )
2107
+ return
2108
+
2109
+ if mode == "status":
2110
+ run_dir = head_run_dir(_project_root)
2111
+ report = read_report(run_dir) if run_dir else {}
2112
+ summary = (
2113
+ dict((report or {}).get("compliance_summary", {}) or {})
2114
+ if isinstance(report, dict)
2115
+ else {}
2116
+ )
2117
+ gate = dict((report or {}).get("gate", {}) or {}) if isinstance(report, dict) else {}
2118
+ console.print("[bold]Compliance status[/bold]")
2119
+ console.print(
2120
+ f" selection: frameworks={list(frameworks)} level={level} "
2121
+ f"rollout={rollout_mode} canary_percent={canary_percent}"
2122
+ )
2123
+ table = Table(title="Compliance packs", show_lines=False)
2124
+ table.add_column("pack_id", no_wrap=True)
2125
+ table.add_column("framework", no_wrap=True)
2126
+ table.add_column("version", no_wrap=True)
2127
+ table.add_column("rules", no_wrap=True)
2128
+ for pack in resolution.packs:
2129
+ table.add_row(pack.pack_id, pack.framework, pack.version, str(len(pack.rules)))
2130
+ console.print(table)
2131
+ if not run_dir:
2132
+ console.print(
2133
+ "[yellow]No runs yet.[/yellow] Run `refactor review` to produce compliance evidence."
2134
+ )
2135
+ return
2136
+ run_id = str((report or {}).get("run_id", run_dir.name))
2137
+ created = str((report or {}).get("created_at", ""))
2138
+ gate_status = str(gate.get("status", "unknown"))
2139
+ projected = int(summary.get("projected_count", 0) or 0)
2140
+ blocked = int(summary.get("blocked_count", 0) or 0)
2141
+ warned = int(summary.get("warned_count", 0) or 0)
2142
+ gate_color = "green" if gate_status == "passed" else "red"
2143
+ console.print(
2144
+ f" latest_run: {run_id}" + (f" ({created})" if created else "")
2145
+ )
2146
+ console.print(
2147
+ f" gate: [{gate_color}]{gate_status}[/{gate_color}] "
2148
+ f"projected={projected} blocked={blocked} warned={warned}"
2149
+ )
2150
+ if blocked > 0 and level in {"standard", "strict"}:
2151
+ console.print(
2152
+ "[yellow]next step:[/yellow] resolve blocked controls or run with "
2153
+ "`--compliance-rollout staged` during pilot."
2154
+ )
2155
+ elif projected == 0:
2156
+ console.print(
2157
+ "[dim]No compliance-projected controls were triggered in the latest run.[/dim]"
2158
+ )
2159
+ return
2160
+
2161
+ if mode == "show":
2162
+ console.print(
2163
+ "[bold]Compliance selection[/bold]: "
2164
+ f"frameworks={list(frameworks)} level={level} "
2165
+ f"rollout={rollout_mode} canary_percent={canary_percent}"
2166
+ )
2167
+ table = Table(title="Compliance packs", show_lines=False)
2168
+ table.add_column("pack_id", no_wrap=True)
2169
+ table.add_column("framework", no_wrap=True)
2170
+ table.add_column("version", no_wrap=True)
2171
+ table.add_column("rules", no_wrap=True)
2172
+ table.add_column("depends_on")
2173
+ for pack in resolution.packs:
2174
+ table.add_row(
2175
+ pack.pack_id,
2176
+ pack.framework,
2177
+ pack.version,
2178
+ str(len(pack.rules)),
2179
+ ", ".join(pack.depends_on) if pack.depends_on else "-",
2180
+ )
2181
+ console.print(table)
2182
+ for warning in resolution.warnings:
2183
+ console.print(f"[yellow]warning:[/yellow] {warning}")
2184
+ return
2185
+
2186
+ if mode == "export":
2187
+ run_dir = head_run_dir(_project_root)
2188
+ if not run_dir:
2189
+ console.print("[yellow]No runs available to export compliance evidence.[/yellow]")
2190
+ raise typer.Exit(code=1)
2191
+ report = read_report(run_dir) or {}
2192
+ payload = build_compliance_evidence_bundle(report)
2193
+ destination = Path(output).expanduser() if output.strip() else (run_dir / "compliance-evidence.json")
2194
+ try:
2195
+ destination.parent.mkdir(parents=True, exist_ok=True)
2196
+ destination.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
2197
+ except OSError as exc:
2198
+ console.print(f"[red]Failed to write compliance evidence:[/red] {exc}")
2199
+ raise typer.Exit(code=1) from exc
2200
+ console.print(f"[green]Compliance evidence exported:[/green] {destination}")
2201
+ return
2202
+
2203
+ if mode == "coverage":
2204
+ table = Table(title="Compliance coverage", show_lines=False)
2205
+ table.add_column("framework", no_wrap=True)
2206
+ table.add_column("control", no_wrap=True)
2207
+ table.add_column("classification", no_wrap=True)
2208
+ rows = 0
2209
+ for pack in resolution.packs:
2210
+ if not pack.coverage:
2211
+ table.add_row(pack.framework, "-", "unknown")
2212
+ rows += 1
2213
+ continue
2214
+ for control, classification in sorted(pack.coverage.items()):
2215
+ table.add_row(pack.framework, control, classification)
2216
+ rows += 1
2217
+ if rows == 0:
2218
+ console.print("[dim]No coverage metadata available in selected packs.[/dim]")
2219
+ return
2220
+ console.print(table)
2221
+ return
2222
+
2223
+ # explain
2224
+ rule_id = (subject or "").strip()
2225
+ if not rule_id:
2226
+ console.print("[red]Rule id is required for `refactor compliance explain`.[/red]")
2227
+ raise typer.Exit(code=2)
2228
+ needle = rule_id.lower()
2229
+ matches = []
2230
+ for pack in resolution.packs:
2231
+ for rule in pack.rules:
2232
+ if rule.rule_id.lower() == needle or needle in rule.rule_id.lower():
2233
+ matches.append((pack, rule))
2234
+ if not matches:
2235
+ console.print(f"[yellow]No compliance rule found:[/yellow] {rule_id}")
2236
+ raise typer.Exit(code=1)
2237
+ for pack, rule in matches:
2238
+ console.print(
2239
+ f"[bold]{rule.rule_id}[/bold] "
2240
+ f"[dim](pack={pack.pack_id}@{pack.version}, framework={pack.framework})[/dim]"
2241
+ )
2242
+ if rule.source_standard:
2243
+ console.print(f" standard: {rule.source_standard}")
2244
+ if rule.control_objective:
2245
+ console.print(f" objective: {rule.control_objective}")
2246
+ console.print(f" operation_intent: {rule.operation_intent}")
2247
+ console.print(f" base_severity: {rule.base_severity}")
2248
+ console.print(f" priority: {rule.priority}")
2249
+
2250
+
1829
2251
  def _format_operation_scope(rr) -> str:
1830
2252
  scope = getattr(rr, "scope", None)
1831
2253
  if scope is None:
@@ -1861,6 +2283,11 @@ def _replace_requests_with_framework_guardrails(
1861
2283
  project_root: Path,
1862
2284
  findings: list[Finding],
1863
2285
  run_id: str,
2286
+ compliance_frameworks: tuple[str, ...] = (),
2287
+ compliance_level: str = "standard",
2288
+ compliance_rollout: str = "default",
2289
+ compliance_canary_percent: int = 20,
2290
+ compliance_resolution: CompliancePackResolution | None = None,
1864
2291
  artifact: RunArtifact | None = None,
1865
2292
  ) -> list:
1866
2293
  clear_requests(project_root)
@@ -1868,20 +2295,79 @@ def _replace_requests_with_framework_guardrails(
1868
2295
  project_root,
1869
2296
  findings,
1870
2297
  run_id=run_id,
2298
+ compliance_frameworks=compliance_frameworks,
2299
+ compliance_level=compliance_level,
2300
+ compliance_rollout=compliance_rollout,
2301
+ compliance_canary_percent=compliance_canary_percent,
1871
2302
  return_diagnostics=True,
1872
2303
  )
1873
2304
  _report_framework_suppressions(suppressions, artifact=artifact)
1874
2305
  _report_framework_warnings(warnings, artifact=artifact)
2306
+ if artifact is not None and compliance_frameworks:
2307
+ compliance_suppressed = [
2308
+ item for item in suppressions if str(item.reason_code).startswith("RR_COMPLIANCE_")
2309
+ ]
2310
+ compliance_warnings = [
2311
+ item for item in warnings if str(item.reason_code).startswith("RR_COMPLIANCE_")
2312
+ ]
2313
+ artifact.compliance_summary = {
2314
+ "level": compliance_level,
2315
+ "frameworks": list(compliance_frameworks),
2316
+ "rollout": compliance_rollout,
2317
+ "canary_percent": int(compliance_canary_percent),
2318
+ "pack_versions": (
2319
+ compliance_resolution.pack_versions if compliance_resolution is not None else {}
2320
+ ),
2321
+ "projected_count": len(compliance_suppressed) + len(compliance_warnings),
2322
+ "blocked_count": len(compliance_suppressed),
2323
+ "warned_count": len(compliance_warnings),
2324
+ "rules_triggered": sorted(
2325
+ {
2326
+ item.rule_id
2327
+ for item in [*compliance_suppressed, *compliance_warnings]
2328
+ if str(item.rule_id).strip()
2329
+ }
2330
+ ),
2331
+ }
1875
2332
  return requests
1876
2333
 
1877
2334
 
2335
+ def _apply_compliance_gate_to_artifact(artifact: RunArtifact) -> None:
2336
+ """Promote compliance blocking decisions into gate outcome (R18 Phase 7)."""
2337
+ summary = dict(getattr(artifact, "compliance_summary", {}) or {})
2338
+ decision = evaluate_compliance_gate(
2339
+ summary,
2340
+ base_gate_status=str(getattr(artifact.gate, "status", "unknown") or "unknown"),
2341
+ )
2342
+ if decision.status == "blocked":
2343
+ artifact.gate.status = "blocked"
2344
+ reasons = list((artifact.gate.blocking_reasons or {}).get("reasons", []))
2345
+ for reason in decision.reasons:
2346
+ if reason not in reasons:
2347
+ reasons.append(reason)
2348
+ artifact.gate.blocking_reasons["reasons"] = reasons
2349
+
2350
+
1878
2351
  def _report_framework_suppressions(
1879
2352
  suppressions: list[RequestSuppression], *, artifact: RunArtifact | None = None
1880
2353
  ) -> None:
1881
2354
  if not suppressions:
1882
2355
  return
2356
+ feasibility = sum(1 for s in suppressions if s.rule_id == "operation_feasibility")
2357
+ compliance = sum(
2358
+ 1 for s in suppressions if s.reason_code.startswith("RR_COMPLIANCE_")
2359
+ )
2360
+ framework = len(suppressions) - feasibility - compliance
2361
+ parts: list[str] = []
2362
+ if framework:
2363
+ parts.append(f"{framework} due to framework invariants")
2364
+ if compliance:
2365
+ parts.append(f"{compliance} due to compliance controls")
2366
+ if feasibility:
2367
+ parts.append(f"{feasibility} not executable as written")
2368
+ detail = "; ".join(parts) if parts else "review-stage checks"
1883
2369
  console.print(
1884
- f"[yellow]warning:[/yellow] suppressed {len(suppressions)} finding(s) due to framework invariants."
2370
+ f"[yellow]warning:[/yellow] suppressed {len(suppressions)} finding(s) ({detail})."
1885
2371
  )
1886
2372
  for item in suppressions:
1887
2373
  console.print(
@@ -1897,8 +2383,16 @@ def _report_framework_warnings(
1897
2383
  ) -> None:
1898
2384
  if not warnings:
1899
2385
  return
2386
+ compliance = sum(1 for item in warnings if item.reason_code.startswith("RR_COMPLIANCE_"))
2387
+ framework = len(warnings) - compliance
2388
+ detail_parts: list[str] = []
2389
+ if framework:
2390
+ detail_parts.append(f"{framework} framework")
2391
+ if compliance:
2392
+ detail_parts.append(f"{compliance} compliance")
2393
+ detail = ", ".join(detail_parts) if detail_parts else "review"
1900
2394
  console.print(
1901
- f"[yellow]warning:[/yellow] flagged {len(warnings)} finding(s) with framework caution."
2395
+ f"[yellow]warning:[/yellow] flagged {len(warnings)} finding(s) with caution ({detail})."
1902
2396
  )
1903
2397
  for item in warnings:
1904
2398
  console.print(
@@ -2166,6 +2660,11 @@ def gc(
2166
2660
 
2167
2661
  def check(
2168
2662
  strict: bool = typer.Option(False, "--strict", help="Exit non-zero if issues are found (for CI)."),
2663
+ compliance_gate: bool = typer.Option(
2664
+ False,
2665
+ "--compliance-gate",
2666
+ help="Also enforce latest run compliance gate status (for CI/PR pipelines).",
2667
+ ),
2169
2668
  ) -> None:
2170
2669
  """Check the constitution for committed secrets (developer/provider keys)."""
2171
2670
  files = find_project_files()
@@ -2176,6 +2675,25 @@ def check(
2176
2675
  constitution = load_constitution(consti_path)
2177
2676
  config = load_config(config_path)
2178
2677
  findings = raw_secret_matches(constitution) + raw_secret_matches_config(config)
2678
+ compliance_failed = False
2679
+ if compliance_gate:
2680
+ run_dir = head_run_dir(consti_path.parent)
2681
+ if run_dir is None:
2682
+ console.print("[yellow]Compliance gate check skipped:[/yellow] no runs yet.")
2683
+ else:
2684
+ report = read_report(run_dir) or {}
2685
+ decision = evaluate_compliance_gate(
2686
+ report.get("compliance_summary", {}),
2687
+ base_gate_status=str((report.get("gate") or {}).get("status", "unknown")),
2688
+ )
2689
+ if decision.status == "blocked":
2690
+ compliance_failed = True
2691
+ console.print(
2692
+ "[red]Compliance gate blocked[/red]: "
2693
+ + ", ".join(decision.reasons or ("compliance_controls_blocked",))
2694
+ )
2695
+ else:
2696
+ console.print("[green]Compliance gate[/green]: passed")
2179
2697
  if findings:
2180
2698
  kinds = ", ".join(sorted({kind for kind, _ in findings}))
2181
2699
  console.print(
@@ -2183,6 +2701,8 @@ def check(
2183
2701
  f"{consti_path} or {config_path}. Replace secrets with `${{ENV_VAR}}` references."
2184
2702
  )
2185
2703
  raise typer.Exit(code=1 if strict else 0)
2704
+ if compliance_failed:
2705
+ raise typer.Exit(code=1 if strict else 0)
2186
2706
  console.print("[green]OK[/green]: no committed secret-like literals detected.")
2187
2707
 
2188
2708
 
@@ -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.19
4
4
  Summary: Local-first CLI for the refactor platform
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown