refactorai-cli 0.3.13__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/PKG-INFO +2 -2
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/pyproject.toml +2 -2
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/__init__.py +1 -1
- refactorai_cli-0.4.0/refactorai_cli/commands/hook_cmds.py +197 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/commands/run_cmds.py +228 -2
- refactorai_cli-0.4.0/refactorai_cli/commands/watch_cmds.py +321 -0
- refactorai_cli-0.4.0/refactorai_cli/commit_queue.py +138 -0
- refactorai_cli-0.4.0/refactorai_cli/git_scope.py +215 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/main.py +11 -0
- refactorai_cli-0.4.0/refactorai_cli/review_runner.py +137 -0
- refactorai_cli-0.4.0/refactorai_cli/watch_state.py +318 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli.egg-info/PKG-INFO +2 -2
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli.egg-info/SOURCES.txt +7 -1
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli.egg-info/requires.txt +1 -1
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/README.md +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/auth.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/client.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/cloud_rr.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/commands/__init__.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/commands/account_cmds.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/commands/auth_cmds.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/commands/cloud_cmds.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/commands/engine_cmds.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/commands/model_cmds.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/commands/rules_cmds.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/commands/runtime_cmds.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/commands/setup_cmds.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/commands/toolchains_cmds.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/control_plane.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/credentials.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/local_constitution.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/local_engine_runtime.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/local_paths.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/model_policy.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/runtime_manager.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/settings.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli/setup_flow.py +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli.egg-info/dependency_links.txt +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli.egg-info/entry_points.txt +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/refactorai_cli.egg-info/top_level.txt +0 -0
- {refactorai_cli-0.3.13 → refactorai_cli-0.4.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: refactorai-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Local-first CLI for the refactor platform
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -8,7 +8,7 @@ Requires-Dist: typer>=0.12.0
|
|
|
8
8
|
Requires-Dist: httpx>=0.27.0
|
|
9
9
|
Requires-Dist: rich>=13.7.0
|
|
10
10
|
Requires-Dist: PyYAML>=6.0.1
|
|
11
|
-
Requires-Dist: refactorai-core>=3.
|
|
11
|
+
Requires-Dist: refactorai-core>=3.2.0
|
|
12
12
|
|
|
13
13
|
# refactorai-cli
|
|
14
14
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "refactorai-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Local-first CLI for the refactor platform"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -12,7 +12,7 @@ dependencies = [
|
|
|
12
12
|
"httpx>=0.27.0",
|
|
13
13
|
"rich>=13.7.0",
|
|
14
14
|
"PyYAML>=6.0.1",
|
|
15
|
-
"refactorai-core>=3.
|
|
15
|
+
"refactorai-core>=3.2.0",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
[project.scripts]
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""`refactor hook` commands: local commit-trigger install + enqueue (docs/41 `R28`).
|
|
2
|
+
|
|
3
|
+
Phase 2 of the commit-triggered Refactor Terminal. ``install`` wires a
|
|
4
|
+
non-blocking ``post-commit`` git hook (via ``core.hooksPath``) that enqueues each
|
|
5
|
+
commit; ``enqueue`` is what that hook calls; ``status``/``uninstall`` manage it.
|
|
6
|
+
|
|
7
|
+
The hook and these commands live in the thin CLI (not the proprietary runtime)
|
|
8
|
+
so enqueuing is fast, offline, and never requires auth or a project config.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
|
|
19
|
+
from refactorai_cli import commit_queue, git_scope
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
app = typer.Typer(
|
|
23
|
+
name="hook",
|
|
24
|
+
help="Manage the local commit-triggered review hook (docs/41).",
|
|
25
|
+
no_args_is_help=True,
|
|
26
|
+
add_completion=False,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_HOOK_MARKER = "RefactorAI post-commit hook"
|
|
30
|
+
_DEFAULT_HOOKS_DIR = ".githooks"
|
|
31
|
+
|
|
32
|
+
# Non-blocking + fail-open: capture HEAD, then detach the enqueue so `git commit`
|
|
33
|
+
# returns immediately. Skip-detection (merge/rebase/opt-out) happens in `enqueue`.
|
|
34
|
+
_POST_COMMIT_SCRIPT = """#!/usr/bin/env bash
|
|
35
|
+
# RefactorAI post-commit hook (docs/41 R28, Phase 2).
|
|
36
|
+
# Non-blocking + fail-open: this hook must never delay or fail `git commit`.
|
|
37
|
+
if ! command -v refactor >/dev/null 2>&1; then
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
_refactor_sha="$(git rev-parse HEAD 2>/dev/null)"
|
|
41
|
+
if [ -z "$_refactor_sha" ]; then
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
# Detach so the commit returns instantly; the enqueue does its own skip checks.
|
|
45
|
+
( refactor hook enqueue --sha "$_refactor_sha" >/dev/null 2>&1 & ) >/dev/null 2>&1 || true
|
|
46
|
+
exit 0
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _require_repo(project_root: Path) -> str:
|
|
51
|
+
root = git_scope.repo_root(project_root)
|
|
52
|
+
if not root:
|
|
53
|
+
console.print("[red]hook:[/red] not inside a git repository.")
|
|
54
|
+
raise typer.Exit(code=1)
|
|
55
|
+
return root
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@app.command("install")
|
|
59
|
+
def install(
|
|
60
|
+
hooks_dir: str | None = typer.Option(
|
|
61
|
+
None,
|
|
62
|
+
"--hooks-dir",
|
|
63
|
+
help=f"Directory to hold git hooks (default: existing core.hooksPath or {_DEFAULT_HOOKS_DIR}).",
|
|
64
|
+
),
|
|
65
|
+
force: bool = typer.Option(
|
|
66
|
+
False,
|
|
67
|
+
"--force",
|
|
68
|
+
help="Overwrite an existing non-RefactorAI post-commit hook.",
|
|
69
|
+
),
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Install the non-blocking post-commit hook and wire ``core.hooksPath``."""
|
|
72
|
+
root = Path(_require_repo(Path.cwd()))
|
|
73
|
+
|
|
74
|
+
existing_path = git_scope.get_config(root, "core.hooksPath")
|
|
75
|
+
if hooks_dir:
|
|
76
|
+
rel_dir = hooks_dir
|
|
77
|
+
elif existing_path:
|
|
78
|
+
rel_dir = existing_path
|
|
79
|
+
else:
|
|
80
|
+
rel_dir = _DEFAULT_HOOKS_DIR
|
|
81
|
+
|
|
82
|
+
hooks_path = (root / rel_dir).resolve()
|
|
83
|
+
hook_file = hooks_path / "post-commit"
|
|
84
|
+
|
|
85
|
+
if hook_file.exists() and _HOOK_MARKER not in _safe_read(hook_file) and not force:
|
|
86
|
+
console.print(
|
|
87
|
+
f"[yellow]hook:[/yellow] a post-commit hook already exists at {hook_file}.\n"
|
|
88
|
+
"Re-run with [bold]--force[/bold] to overwrite, or point --hooks-dir elsewhere."
|
|
89
|
+
)
|
|
90
|
+
raise typer.Exit(code=1)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
hooks_path.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
hook_file.write_text(_POST_COMMIT_SCRIPT, encoding="utf-8")
|
|
95
|
+
hook_file.chmod(hook_file.stat().st_mode | 0o755)
|
|
96
|
+
except OSError as exc:
|
|
97
|
+
console.print(f"[red]hook: could not write hook:[/red] {exc}")
|
|
98
|
+
raise typer.Exit(code=1) from exc
|
|
99
|
+
|
|
100
|
+
if not git_scope.set_config(root, "core.hooksPath", rel_dir):
|
|
101
|
+
console.print("[red]hook: could not set core.hooksPath.[/red]")
|
|
102
|
+
raise typer.Exit(code=1)
|
|
103
|
+
|
|
104
|
+
console.print(
|
|
105
|
+
f"[green]Installed[/green] post-commit hook -> {hook_file}\n"
|
|
106
|
+
f"[dim]core.hooksPath = {rel_dir}[/dim]\n"
|
|
107
|
+
"Every commit now enqueues a local review event. "
|
|
108
|
+
"Open the Refactor Terminal with `refactor watch` (coming in Phase 3)."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@app.command("uninstall")
|
|
113
|
+
def uninstall(
|
|
114
|
+
reset_hooks_path: bool = typer.Option(
|
|
115
|
+
False,
|
|
116
|
+
"--reset-hooks-path",
|
|
117
|
+
help="Also unset core.hooksPath (only do this if RefactorAI set it).",
|
|
118
|
+
),
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Remove the RefactorAI post-commit hook."""
|
|
121
|
+
root = Path(_require_repo(Path.cwd()))
|
|
122
|
+
rel_dir = git_scope.get_config(root, "core.hooksPath") or _DEFAULT_HOOKS_DIR
|
|
123
|
+
hook_file = (root / rel_dir / "post-commit").resolve()
|
|
124
|
+
|
|
125
|
+
removed = False
|
|
126
|
+
if hook_file.exists() and _HOOK_MARKER in _safe_read(hook_file):
|
|
127
|
+
try:
|
|
128
|
+
hook_file.unlink()
|
|
129
|
+
removed = True
|
|
130
|
+
except OSError as exc:
|
|
131
|
+
console.print(f"[red]hook: could not remove hook:[/red] {exc}")
|
|
132
|
+
raise typer.Exit(code=1) from exc
|
|
133
|
+
|
|
134
|
+
if reset_hooks_path:
|
|
135
|
+
git_scope.unset_config(root, "core.hooksPath")
|
|
136
|
+
|
|
137
|
+
if removed:
|
|
138
|
+
console.print("[green]Removed[/green] the RefactorAI post-commit hook.")
|
|
139
|
+
else:
|
|
140
|
+
console.print("[dim]hook: no RefactorAI post-commit hook found to remove.[/dim]")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@app.command("enqueue")
|
|
144
|
+
def enqueue(
|
|
145
|
+
sha: str = typer.Option("HEAD", "--sha", help="Commit SHA/ref to enqueue."),
|
|
146
|
+
verbose: bool = typer.Option(False, "--verbose", help="Print the enqueue result."),
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Enqueue a commit event (invoked by the post-commit hook). Always exits 0."""
|
|
149
|
+
project_root = Path(os.environ.get("REFACTOR_PROJECT_ROOT") or Path.cwd())
|
|
150
|
+
try:
|
|
151
|
+
enqueued, reason, event = commit_queue.enqueue_commit_event(project_root, sha)
|
|
152
|
+
except Exception as exc: # fail-open: never let enqueue affect the commit flow
|
|
153
|
+
if verbose:
|
|
154
|
+
console.print(f"[yellow]hook enqueue error (ignored):[/yellow] {exc}")
|
|
155
|
+
return
|
|
156
|
+
if verbose:
|
|
157
|
+
if enqueued and event:
|
|
158
|
+
console.print(
|
|
159
|
+
f"[green]enqueued[/green] {event['sha'][:10]} "
|
|
160
|
+
f"({len(event['changed_files'])} changed file(s)) -> "
|
|
161
|
+
f"{commit_queue.queue_path(project_root)}"
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
console.print(f"[dim]hook: not enqueued ({reason}).[/dim]")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@app.command("status")
|
|
168
|
+
def status() -> None:
|
|
169
|
+
"""Show hook installation state and the pending commit-event count."""
|
|
170
|
+
root_str = git_scope.repo_root(Path.cwd())
|
|
171
|
+
if not root_str:
|
|
172
|
+
console.print("[yellow]hook:[/yellow] not inside a git repository.")
|
|
173
|
+
raise typer.Exit(code=1)
|
|
174
|
+
root = Path(root_str)
|
|
175
|
+
|
|
176
|
+
rel_dir = git_scope.get_config(root, "core.hooksPath")
|
|
177
|
+
hook_file = (root / (rel_dir or _DEFAULT_HOOKS_DIR) / "post-commit").resolve()
|
|
178
|
+
is_ours = hook_file.exists() and _HOOK_MARKER in _safe_read(hook_file)
|
|
179
|
+
q_path = commit_queue.queue_path(root)
|
|
180
|
+
|
|
181
|
+
console.print(f"Repository: [bold]{root}[/bold]")
|
|
182
|
+
console.print(f"core.hooksPath: {rel_dir or '[dim](unset)[/dim]'}")
|
|
183
|
+
if is_ours:
|
|
184
|
+
console.print(f"post-commit hook: [green]installed[/green] ({hook_file})")
|
|
185
|
+
elif hook_file.exists():
|
|
186
|
+
console.print(f"post-commit hook: [yellow]present but not RefactorAI[/yellow] ({hook_file})")
|
|
187
|
+
else:
|
|
188
|
+
console.print("post-commit hook: [red]not installed[/red] (run `refactor hook install`)")
|
|
189
|
+
console.print(f"Queue: {q_path}")
|
|
190
|
+
console.print(f"Pending events: [bold]{commit_queue.pending_count(root)}[/bold]")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _safe_read(path: Path) -> str:
|
|
194
|
+
try:
|
|
195
|
+
return path.read_text(encoding="utf-8")
|
|
196
|
+
except OSError:
|
|
197
|
+
return ""
|
|
@@ -1800,6 +1800,134 @@ def shell(
|
|
|
1800
1800
|
raise typer.Exit(code=1)
|
|
1801
1801
|
|
|
1802
1802
|
|
|
1803
|
+
def _graph_expand_hops(constitution) -> int:
|
|
1804
|
+
"""Graph expansion depth for commit scope (constitution intelligence)."""
|
|
1805
|
+
intel = constitution.get_setting("intelligence", {}) if constitution else {}
|
|
1806
|
+
if isinstance(intel, dict):
|
|
1807
|
+
try:
|
|
1808
|
+
return max(0, int(intel.get("graph_expand_hops", 1) or 1))
|
|
1809
|
+
except (TypeError, ValueError):
|
|
1810
|
+
return 1
|
|
1811
|
+
return 1
|
|
1812
|
+
|
|
1813
|
+
|
|
1814
|
+
def _associated_files_for(project_root: Path, changed: list[str], hops: int) -> list[str]:
|
|
1815
|
+
"""Files graph-associated with ``changed`` via the persisted symbol graph.
|
|
1816
|
+
|
|
1817
|
+
Best-effort: returns ``[]`` when no graph is available or the changed files
|
|
1818
|
+
have no matching symbols, so review simply falls back to the changed files.
|
|
1819
|
+
"""
|
|
1820
|
+
if not changed:
|
|
1821
|
+
return []
|
|
1822
|
+
try:
|
|
1823
|
+
from refactor_core.codeintel.context import associated_files
|
|
1824
|
+
from refactor_core.codeintel.graph import SymbolGraph
|
|
1825
|
+
from refactor_core.store import read_symbol_graph
|
|
1826
|
+
|
|
1827
|
+
graph_dict = read_symbol_graph(project_root)
|
|
1828
|
+
if not graph_dict:
|
|
1829
|
+
return []
|
|
1830
|
+
graph = SymbolGraph.from_dict(graph_dict)
|
|
1831
|
+
return associated_files(graph, changed, hops=hops)
|
|
1832
|
+
except Exception:
|
|
1833
|
+
return []
|
|
1834
|
+
|
|
1835
|
+
|
|
1836
|
+
def _resolve_commit_scope(
|
|
1837
|
+
*,
|
|
1838
|
+
project_root: Path,
|
|
1839
|
+
constitution,
|
|
1840
|
+
commit: str | None,
|
|
1841
|
+
changed: bool,
|
|
1842
|
+
intent: str | None,
|
|
1843
|
+
) -> dict | None:
|
|
1844
|
+
"""Resolve commit/working-tree scope for a review (docs/41 `R28`, Phase 1).
|
|
1845
|
+
|
|
1846
|
+
Returns ``None`` when no scoping was requested. Otherwise a dict with:
|
|
1847
|
+
``scope_paths`` (changed + associated, or ``None`` for a full-project
|
|
1848
|
+
baseline), ``changed_paths``, ``intent`` (canonical contract or ``None``),
|
|
1849
|
+
``summary`` (one-line intent), and ``note`` (human scope description).
|
|
1850
|
+
"""
|
|
1851
|
+
if not commit and not changed:
|
|
1852
|
+
return None
|
|
1853
|
+
|
|
1854
|
+
from refactorai_cli import git_scope
|
|
1855
|
+
from refactor_core.intent import capture as intent_capture
|
|
1856
|
+
|
|
1857
|
+
if not git_scope.is_git_repo(project_root):
|
|
1858
|
+
console.print("[yellow]review: --commit/--changed requires a git repository; ignoring scope.[/yellow]")
|
|
1859
|
+
return None
|
|
1860
|
+
|
|
1861
|
+
sources: list[dict] = []
|
|
1862
|
+
changed_files: list[str] = []
|
|
1863
|
+
full_project = False
|
|
1864
|
+
scope_label = ""
|
|
1865
|
+
|
|
1866
|
+
if commit:
|
|
1867
|
+
sha = git_scope.resolve_sha(project_root, commit)
|
|
1868
|
+
if not sha:
|
|
1869
|
+
console.print(f"[red]review: could not resolve commit '{commit}'.[/red]")
|
|
1870
|
+
raise typer.Exit(code=2)
|
|
1871
|
+
subject, message = git_scope.commit_message(project_root, sha)
|
|
1872
|
+
if subject or message:
|
|
1873
|
+
sources.append(
|
|
1874
|
+
{"type": "commit_message", "title": subject, "text": message, "ref": sha}
|
|
1875
|
+
)
|
|
1876
|
+
if git_scope.is_first_commit(project_root, sha):
|
|
1877
|
+
full_project = True
|
|
1878
|
+
scope_label = f"first commit {sha[:10]} -> full project"
|
|
1879
|
+
else:
|
|
1880
|
+
changed_files = git_scope.commit_changed_files(project_root, sha)
|
|
1881
|
+
scope_label = f"commit {sha[:10]}"
|
|
1882
|
+
elif changed:
|
|
1883
|
+
changed_files = git_scope.working_tree_changed_files(project_root)
|
|
1884
|
+
scope_label = "working tree"
|
|
1885
|
+
|
|
1886
|
+
if intent and intent.strip():
|
|
1887
|
+
sources.append({"type": "agent_prompt", "text": intent.strip()})
|
|
1888
|
+
|
|
1889
|
+
canonical = intent_capture.derive_contract(sources) if sources else None
|
|
1890
|
+
summary = intent_capture.summarize_intent(canonical)
|
|
1891
|
+
|
|
1892
|
+
if full_project:
|
|
1893
|
+
return {
|
|
1894
|
+
"scope_paths": None,
|
|
1895
|
+
"changed_paths": None,
|
|
1896
|
+
"intent": canonical,
|
|
1897
|
+
"summary": summary,
|
|
1898
|
+
"note": scope_label,
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
# Restrict to files that still exist so a deleted path never becomes a target.
|
|
1902
|
+
existing_changed = [p for p in changed_files if (project_root / p).is_file()]
|
|
1903
|
+
if not existing_changed:
|
|
1904
|
+
console.print(
|
|
1905
|
+
f"[dim]review: no reviewable changed files in {scope_label}; "
|
|
1906
|
+
"falling back to full target.[/dim]"
|
|
1907
|
+
)
|
|
1908
|
+
return {
|
|
1909
|
+
"scope_paths": None,
|
|
1910
|
+
"changed_paths": None,
|
|
1911
|
+
"intent": canonical,
|
|
1912
|
+
"summary": summary,
|
|
1913
|
+
"note": f"{scope_label} (no changed files)",
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
associated = _associated_files_for(project_root, existing_changed, _graph_expand_hops(constitution))
|
|
1917
|
+
scope_paths: list[str] = []
|
|
1918
|
+
for path in [*existing_changed, *associated]:
|
|
1919
|
+
if path not in scope_paths:
|
|
1920
|
+
scope_paths.append(path)
|
|
1921
|
+
|
|
1922
|
+
return {
|
|
1923
|
+
"scope_paths": scope_paths,
|
|
1924
|
+
"changed_paths": existing_changed,
|
|
1925
|
+
"intent": canonical,
|
|
1926
|
+
"summary": summary,
|
|
1927
|
+
"note": f"{scope_label}: {len(existing_changed)} changed + {len(associated)} associated",
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
|
|
1803
1931
|
def review(
|
|
1804
1932
|
target: str = typer.Argument(".", help="File, directory, or symbol to review."),
|
|
1805
1933
|
provider: str | None = typer.Option(
|
|
@@ -1817,6 +1945,25 @@ def review(
|
|
|
1817
1945
|
"--force",
|
|
1818
1946
|
help="Run review even if unapplied patches exist from a prior code run.",
|
|
1819
1947
|
),
|
|
1948
|
+
commit: str | None = typer.Option(
|
|
1949
|
+
None,
|
|
1950
|
+
"--commit",
|
|
1951
|
+
help="Scope the review to a commit's changed files + graph-associated "
|
|
1952
|
+
"files (local only, docs/41). Use a SHA/ref; the repo root commit falls "
|
|
1953
|
+
"back to a full-project review.",
|
|
1954
|
+
),
|
|
1955
|
+
changed: bool = typer.Option(
|
|
1956
|
+
False,
|
|
1957
|
+
"--changed",
|
|
1958
|
+
help="Scope the review to the working tree's changed files + "
|
|
1959
|
+
"graph-associated files (local only).",
|
|
1960
|
+
),
|
|
1961
|
+
intent: str | None = typer.Option(
|
|
1962
|
+
None,
|
|
1963
|
+
"--intent",
|
|
1964
|
+
help="Explicit intent for this change (agent-supplied). Merged with any "
|
|
1965
|
+
"commit-message-derived intent to guide the review.",
|
|
1966
|
+
),
|
|
1820
1967
|
compliance_framework: list[str] | None = typer.Option(
|
|
1821
1968
|
None,
|
|
1822
1969
|
"--compliance-framework",
|
|
@@ -1878,6 +2025,29 @@ def review(
|
|
|
1878
2025
|
console.print(f"[yellow]review: compliance warning:[/yellow] {warning}")
|
|
1879
2026
|
register_project(project_root, constitution_hash=constitution.content_hash)
|
|
1880
2027
|
_maybe_build_graph(project_root, project_config)
|
|
2028
|
+
|
|
2029
|
+
# Commit-scoped local review (docs/41 `R28`). Local-only: the cloud path does
|
|
2030
|
+
# not carry an explicit file scope, so scoping + cloud mode is rejected early.
|
|
2031
|
+
if (commit or changed) and _is_cloud_mode(constitution):
|
|
2032
|
+
console.print(
|
|
2033
|
+
"[red]review: --commit/--changed is local-only[/red] and is not "
|
|
2034
|
+
f"supported in {_cloud_execution_mode(constitution)} mode."
|
|
2035
|
+
)
|
|
2036
|
+
raise typer.Exit(code=2)
|
|
2037
|
+
scope = _resolve_commit_scope(
|
|
2038
|
+
project_root=project_root,
|
|
2039
|
+
constitution=constitution,
|
|
2040
|
+
commit=commit,
|
|
2041
|
+
changed=changed,
|
|
2042
|
+
intent=intent,
|
|
2043
|
+
)
|
|
2044
|
+
scope_paths = scope.get("scope_paths") if scope else None
|
|
2045
|
+
changed_paths = scope.get("changed_paths") if scope else None
|
|
2046
|
+
if scope:
|
|
2047
|
+
console.print(f"[cyan]review scope[/cyan]: {scope['note']}")
|
|
2048
|
+
if scope.get("summary"):
|
|
2049
|
+
console.print(f"[dim]intent:[/dim] {scope['summary']}")
|
|
2050
|
+
|
|
1881
2051
|
sandbox_ctx = None
|
|
1882
2052
|
|
|
1883
2053
|
try:
|
|
@@ -1926,7 +2096,11 @@ def review(
|
|
|
1926
2096
|
cli_mode=mode,
|
|
1927
2097
|
progress_cb=progress_cb,
|
|
1928
2098
|
step_limit_cb=_step_limit_prompt("review"),
|
|
2099
|
+
scope_paths=scope_paths,
|
|
2100
|
+
changed_paths=changed_paths,
|
|
1929
2101
|
)
|
|
2102
|
+
if scope and scope.get("summary"):
|
|
2103
|
+
artifact.notes.append(f"Commit-scoped review intent: {scope['summary']}")
|
|
1930
2104
|
preflight_warning = ""
|
|
1931
2105
|
if isinstance(sandbox_ctx, dict):
|
|
1932
2106
|
preflight_warning = str(sandbox_ctx.get("preflight_warning") or "").strip()
|
|
@@ -2026,6 +2200,24 @@ def code(
|
|
|
2026
2200
|
"--mode",
|
|
2027
2201
|
help="Execution mode: auto | agentic | planner (default resolves from capability).",
|
|
2028
2202
|
),
|
|
2203
|
+
commit: str | None = typer.Option(
|
|
2204
|
+
None,
|
|
2205
|
+
"--commit",
|
|
2206
|
+
help="Scope to a commit's changed + graph-associated files and (re)create "
|
|
2207
|
+
"that commit's refactor requests before resolving them (local only, docs/41). "
|
|
2208
|
+
"Used by the Refactor Terminal's [r] action.",
|
|
2209
|
+
),
|
|
2210
|
+
changed: bool = typer.Option(
|
|
2211
|
+
False,
|
|
2212
|
+
"--changed",
|
|
2213
|
+
help="Scope to the working tree's changed + graph-associated files (local only).",
|
|
2214
|
+
),
|
|
2215
|
+
intent: str | None = typer.Option(
|
|
2216
|
+
None,
|
|
2217
|
+
"--intent",
|
|
2218
|
+
help="Explicit intent for this change (agent-supplied), merged with any "
|
|
2219
|
+
"commit-message-derived intent to guide the review that creates requests.",
|
|
2220
|
+
),
|
|
2029
2221
|
auto_approve: bool = typer.Option(
|
|
2030
2222
|
False,
|
|
2031
2223
|
"--auto-approve",
|
|
@@ -2106,6 +2298,31 @@ def code(
|
|
|
2106
2298
|
console.print(f"[yellow]code: compliance warning:[/yellow] {warning}")
|
|
2107
2299
|
register_project(project_root, constitution_hash=constitution.content_hash)
|
|
2108
2300
|
_maybe_build_graph(project_root, project_config)
|
|
2301
|
+
|
|
2302
|
+
# Commit-scoped resolve (docs/41 `R28`, Phase 4). Local-only, symmetric to
|
|
2303
|
+
# `review`. When scoped, we (re)create THIS commit's requests via a scoped
|
|
2304
|
+
# review-first so `[r]` in the Refactor Terminal resolves the selected
|
|
2305
|
+
# commit's requests rather than a stale pending set.
|
|
2306
|
+
if (commit or changed) and _is_cloud_mode(constitution):
|
|
2307
|
+
console.print(
|
|
2308
|
+
"[red]code: --commit/--changed is local-only[/red] and is not "
|
|
2309
|
+
f"supported in {_cloud_execution_mode(constitution)} mode."
|
|
2310
|
+
)
|
|
2311
|
+
raise typer.Exit(code=2)
|
|
2312
|
+
code_scope = _resolve_commit_scope(
|
|
2313
|
+
project_root=project_root,
|
|
2314
|
+
constitution=constitution,
|
|
2315
|
+
commit=commit,
|
|
2316
|
+
changed=changed,
|
|
2317
|
+
intent=intent,
|
|
2318
|
+
)
|
|
2319
|
+
code_scope_paths = code_scope.get("scope_paths") if code_scope else None
|
|
2320
|
+
code_changed_paths = code_scope.get("changed_paths") if code_scope else None
|
|
2321
|
+
if code_scope:
|
|
2322
|
+
console.print(f"[cyan]code scope[/cyan]: {code_scope['note']}")
|
|
2323
|
+
if code_scope.get("summary"):
|
|
2324
|
+
console.print(f"[dim]intent:[/dim] {code_scope['summary']}")
|
|
2325
|
+
|
|
2109
2326
|
sandbox_ctx = None
|
|
2110
2327
|
|
|
2111
2328
|
try:
|
|
@@ -2138,9 +2355,16 @@ def code(
|
|
|
2138
2355
|
cloud_review_findings: list[Finding] = []
|
|
2139
2356
|
|
|
2140
2357
|
# If no pending refactor requests exist, run review first to create them.
|
|
2141
|
-
|
|
2358
|
+
# A commit/working-tree scope always forces a fresh scoped review so the
|
|
2359
|
+
# requests match the selected commit (docs/41 `R28`, Phase 4).
|
|
2360
|
+
requests = [] if code_scope else pending_requests(project_root)
|
|
2142
2361
|
if not requests:
|
|
2143
|
-
|
|
2362
|
+
if code_scope:
|
|
2363
|
+
console.print(
|
|
2364
|
+
f"[dim]code: creating refactor requests for {code_scope['note']}...[/dim]"
|
|
2365
|
+
)
|
|
2366
|
+
else:
|
|
2367
|
+
console.print("[dim]code: no pending refactor requests; running review first...[/dim]")
|
|
2144
2368
|
if cloud:
|
|
2145
2369
|
review_artifact = _cloud_inference(
|
|
2146
2370
|
stage="review",
|
|
@@ -2166,6 +2390,8 @@ def code(
|
|
|
2166
2390
|
cli_mode=mode,
|
|
2167
2391
|
progress_cb=review_progress,
|
|
2168
2392
|
step_limit_cb=_step_limit_prompt("review"),
|
|
2393
|
+
scope_paths=code_scope_paths,
|
|
2394
|
+
changed_paths=code_changed_paths,
|
|
2169
2395
|
)
|
|
2170
2396
|
review_store = create_run(project_root, run_id=review_artifact.run_id)
|
|
2171
2397
|
review_store.write_artifact(review_artifact, target=target, status="completed")
|