refactorai-cli 0.4.10__tar.gz → 0.4.11__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.4.10 → refactorai_cli-0.4.11}/PKG-INFO +2 -2
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/pyproject.toml +2 -2
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/__init__.py +1 -1
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commands/branch_cmds.py +3 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commands/run_cmds.py +10 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commands/watch_cmds.py +324 -34
- refactorai_cli-0.4.11/refactorai_cli/refactor_branch_store.py +189 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/watch_state.py +289 -13
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli.egg-info/PKG-INFO +2 -2
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli.egg-info/requires.txt +1 -1
- refactorai_cli-0.4.10/refactorai_cli/refactor_branch_store.py +0 -116
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/README.md +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/auth.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/client.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/cloud_rr.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commands/__init__.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commands/account_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commands/auth_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commands/cloud_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commands/engine_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commands/hook_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commands/model_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commands/request_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commands/rules_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commands/runtime_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commands/setup_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commands/toolchains_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/commit_queue.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/control_plane.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/credentials.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/dotenv_loader.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/git_scope.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/local_constitution.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/local_engine_runtime.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/local_paths.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/main.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/model_policy.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/review_runner.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/runtime_manager.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/settings.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli/setup_flow.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli.egg-info/SOURCES.txt +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli.egg-info/dependency_links.txt +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli.egg-info/entry_points.txt +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/refactorai_cli.egg-info/top_level.txt +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.11}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: refactorai-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.11
|
|
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.2.
|
|
11
|
+
Requires-Dist: refactorai-core>=3.2.6
|
|
12
12
|
|
|
13
13
|
# refactorai-cli
|
|
14
14
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "refactorai-cli"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.11"
|
|
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.2.
|
|
15
|
+
"refactorai-core>=3.2.6",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
[project.scripts]
|
|
@@ -128,6 +128,9 @@ def merge_branch(
|
|
|
128
128
|
ok, output = git_scope.merge(project_root, target, squash=squash)
|
|
129
129
|
if not ok:
|
|
130
130
|
return False, f"merge failed / conflicted: {output}"
|
|
131
|
+
# docs/43 `R30`: remember the terminal outcome so the finalize view can render
|
|
132
|
+
# a DONE (merged) node without re-inspecting git.
|
|
133
|
+
refactor_branch_store.set_outcome(project_root, target, refactor_branch_store.OUTCOME_MERGED)
|
|
131
134
|
if squash:
|
|
132
135
|
sha = git_scope.commit(project_root, _squash_message(project_root, target, source))
|
|
133
136
|
if not sha:
|
|
@@ -3217,6 +3217,16 @@ def code(
|
|
|
3217
3217
|
final_status = "skipped"
|
|
3218
3218
|
if _final_test_enabled(project_config):
|
|
3219
3219
|
final_status, _ = _run_final_test_suite(project_root, project_config)
|
|
3220
|
+
# docs/43 `R30`: persist the verdict so the Refactor Terminal finalize view
|
|
3221
|
+
# can show it without re-running the suite.
|
|
3222
|
+
try:
|
|
3223
|
+
from refactorai_cli import refactor_branch_store
|
|
3224
|
+
|
|
3225
|
+
refactor_branch_store.set_final_test(
|
|
3226
|
+
project_root, refactor_branch_ctx.get("branch_name", ""), final_status
|
|
3227
|
+
)
|
|
3228
|
+
except Exception:
|
|
3229
|
+
pass
|
|
3220
3230
|
decision = _decide_finalize(
|
|
3221
3231
|
project_config,
|
|
3222
3232
|
finalize=finalize,
|
|
@@ -25,10 +25,15 @@ from typing import Callable
|
|
|
25
25
|
import typer
|
|
26
26
|
from rich.console import Console
|
|
27
27
|
|
|
28
|
-
from refactorai_cli import commit_queue, git_scope, watch_state
|
|
28
|
+
from refactorai_cli import commit_queue, git_scope, refactor_branch_store, watch_state
|
|
29
29
|
from refactorai_cli.auth import AuthError, ensure_authenticated
|
|
30
30
|
from refactorai_cli.review_runner import ReviewRunner, SubprocessReviewRunner
|
|
31
|
-
from refactorai_cli.watch_state import
|
|
31
|
+
from refactorai_cli.watch_state import (
|
|
32
|
+
FinalizeInfo,
|
|
33
|
+
RRCommit,
|
|
34
|
+
WatchState,
|
|
35
|
+
render_graph,
|
|
36
|
+
)
|
|
32
37
|
|
|
33
38
|
console = Console()
|
|
34
39
|
|
|
@@ -121,6 +126,141 @@ def _pending_for_run(
|
|
|
121
126
|
return len(matches), lines
|
|
122
127
|
|
|
123
128
|
|
|
129
|
+
# --------------------------------------------------------------------------- #
|
|
130
|
+
# Finalize / branch-view resolution (docs/43 `R30`, Phase 1) #
|
|
131
|
+
# --------------------------------------------------------------------------- #
|
|
132
|
+
|
|
133
|
+
def _shas_match(a: str, b: str) -> bool:
|
|
134
|
+
"""True if two shas refer to the same commit (tolerant of short/long forms)."""
|
|
135
|
+
a = str(a or "")
|
|
136
|
+
b = str(b or "")
|
|
137
|
+
if not a or not b:
|
|
138
|
+
return False
|
|
139
|
+
return a == b or a.startswith(b) or b.startswith(a)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _resolve_refactor_branch(
|
|
143
|
+
project_root: Path, node
|
|
144
|
+
) -> tuple[str, "refactor_branch_store.BranchMeta | None"]:
|
|
145
|
+
"""Map a commit node to its refactor branch (docs/43 Part C).
|
|
146
|
+
|
|
147
|
+
Resolution order: (1) a branch-store entry whose ``source_sha`` matches the
|
|
148
|
+
node, (2) the ``refactor/<branch>/<shortsha>`` name template, (3) an RR
|
|
149
|
+
record carrying ``refactor_branch`` for this node's run. Returns
|
|
150
|
+
``("", None)`` when nothing links the node to a refactor branch.
|
|
151
|
+
"""
|
|
152
|
+
# 1) recorded source association (reversible, preferred).
|
|
153
|
+
for meta in refactor_branch_store.list_branches(project_root):
|
|
154
|
+
if meta.source_sha and _shas_match(node.sha, meta.source_sha):
|
|
155
|
+
return meta.branch, meta
|
|
156
|
+
# 2) deterministic name template from the node's branch + sha.
|
|
157
|
+
template = refactor_branch_store.refactor_branch_name(node.branch, node.sha)
|
|
158
|
+
meta = refactor_branch_store.load_branch(project_root, template)
|
|
159
|
+
if meta is not None or git_scope.branch_exists(project_root, template):
|
|
160
|
+
return template, meta
|
|
161
|
+
# 3) RR records created by this node's run.
|
|
162
|
+
branch = _branch_from_run(project_root, getattr(node, "run_id", ""))
|
|
163
|
+
if branch:
|
|
164
|
+
return branch, refactor_branch_store.load_branch(project_root, branch)
|
|
165
|
+
return "", None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _branch_from_run(project_root: Path, run_id: str) -> str:
|
|
169
|
+
"""First ``refactor_branch`` recorded on an RR created by ``run_id``."""
|
|
170
|
+
if not run_id:
|
|
171
|
+
return ""
|
|
172
|
+
try:
|
|
173
|
+
from refactor_core.refactor_requests import list_requests
|
|
174
|
+
except Exception:
|
|
175
|
+
return ""
|
|
176
|
+
try:
|
|
177
|
+
requests = list_requests(project_root)
|
|
178
|
+
except Exception:
|
|
179
|
+
return ""
|
|
180
|
+
for rr in requests:
|
|
181
|
+
if str(getattr(rr, "created_by_run", "") or "") == run_id:
|
|
182
|
+
branch = str(getattr(rr, "refactor_branch", "") or "")
|
|
183
|
+
if branch:
|
|
184
|
+
return branch
|
|
185
|
+
return ""
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _rr_commits_for_branch(
|
|
189
|
+
project_root: Path, branch: str, *, run_id: str = ""
|
|
190
|
+
) -> list[RRCommit]:
|
|
191
|
+
"""Per-request commits recorded on ``branch`` (newest first)."""
|
|
192
|
+
if not branch:
|
|
193
|
+
return []
|
|
194
|
+
try:
|
|
195
|
+
from refactor_core.refactor_requests import list_requests
|
|
196
|
+
except Exception:
|
|
197
|
+
return []
|
|
198
|
+
try:
|
|
199
|
+
requests = list_requests(project_root)
|
|
200
|
+
except Exception:
|
|
201
|
+
return []
|
|
202
|
+
out: list[RRCommit] = []
|
|
203
|
+
for rr in requests:
|
|
204
|
+
rr_branch = str(getattr(rr, "refactor_branch", "") or "")
|
|
205
|
+
commit_sha = str(getattr(rr, "commit_sha", "") or "")
|
|
206
|
+
if not commit_sha:
|
|
207
|
+
continue
|
|
208
|
+
# Prefer an exact branch match; fall back to the node's run when the RR
|
|
209
|
+
# predates branch tagging.
|
|
210
|
+
if rr_branch == branch or (not rr_branch and run_id and str(getattr(rr, "created_by_run", "") or "") == run_id):
|
|
211
|
+
out.append(
|
|
212
|
+
RRCommit(
|
|
213
|
+
rr_id=str(getattr(rr, "rr_id", "") or ""),
|
|
214
|
+
commit_sha=commit_sha,
|
|
215
|
+
location=str(getattr(rr, "location", "") or ""),
|
|
216
|
+
status=str(getattr(rr, "status", "") or ""),
|
|
217
|
+
reverted=bool(getattr(rr, "reverted", False)),
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
# ``list_requests`` is oldest-first; newest RR commit renders on top.
|
|
221
|
+
out.reverse()
|
|
222
|
+
return out
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def resolve_finalize(project_root: Path, node) -> FinalizeInfo | None:
|
|
226
|
+
"""Resolve the finalize/branch-view data for a node (docs/43 Part C/G).
|
|
227
|
+
|
|
228
|
+
Returns ``None`` when the node has no refactor branch to finalize. Fail-soft:
|
|
229
|
+
any store/git hiccup yields ``None`` rather than raising into the terminal.
|
|
230
|
+
"""
|
|
231
|
+
try:
|
|
232
|
+
branch, meta = _resolve_refactor_branch(project_root, node)
|
|
233
|
+
except Exception:
|
|
234
|
+
return None
|
|
235
|
+
if not branch:
|
|
236
|
+
return None
|
|
237
|
+
try:
|
|
238
|
+
exists = git_scope.branch_exists(project_root, branch)
|
|
239
|
+
except Exception:
|
|
240
|
+
exists = False
|
|
241
|
+
commits = _rr_commits_for_branch(project_root, branch, run_id=getattr(node, "run_id", ""))
|
|
242
|
+
outcome = meta.outcome if meta else ""
|
|
243
|
+
# Nothing to show and nothing recorded -> treat as "no finalize".
|
|
244
|
+
if not commits and not exists and not outcome:
|
|
245
|
+
return None
|
|
246
|
+
return FinalizeInfo(
|
|
247
|
+
refactor_branch=branch,
|
|
248
|
+
source_branch=(meta.source_branch if meta else "") or node.branch,
|
|
249
|
+
source_sha=(meta.source_sha if meta else "") or node.sha,
|
|
250
|
+
branch_exists=exists,
|
|
251
|
+
outcome=outcome,
|
|
252
|
+
final_test=meta.final_test if meta else "",
|
|
253
|
+
commits=commits,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def refresh_finalize(project_root: Path, state: WatchState) -> None:
|
|
258
|
+
"""Recompute finalize data for reviewed nodes so phases stay current."""
|
|
259
|
+
for node in state.nodes_oldest_first():
|
|
260
|
+
if node.state == watch_state.REVIEWED:
|
|
261
|
+
node.finalize = resolve_finalize(project_root, node)
|
|
262
|
+
|
|
263
|
+
|
|
124
264
|
def _resolve_repo(project_root: Path | None) -> Path:
|
|
125
265
|
root = git_scope.repo_root(project_root or Path.cwd())
|
|
126
266
|
if not root:
|
|
@@ -184,6 +324,11 @@ def process_pending(
|
|
|
184
324
|
(rendering happens via ``on_update``) so it is unit-testable with a fake
|
|
185
325
|
runner and a temporary queue file.
|
|
186
326
|
"""
|
|
327
|
+
# Track the checked-out branch so the finalize view can mark "(current)".
|
|
328
|
+
try:
|
|
329
|
+
state.current_branch = git_scope.current_branch(project_root) or state.current_branch
|
|
330
|
+
except Exception:
|
|
331
|
+
pass
|
|
187
332
|
for event in commit_queue.read_events(project_root):
|
|
188
333
|
sha = str(event.get("sha") or "")
|
|
189
334
|
if sha and not state.has(sha):
|
|
@@ -210,6 +355,11 @@ def process_pending(
|
|
|
210
355
|
reviewed += 1
|
|
211
356
|
if on_update:
|
|
212
357
|
on_update(state)
|
|
358
|
+
# docs/43 Phase 1: fold current refactor-branch data into reviewed nodes so
|
|
359
|
+
# their phase (review/finalize/done) reflects any branch built by `[r]`.
|
|
360
|
+
refresh_finalize(project_root, state)
|
|
361
|
+
if on_update:
|
|
362
|
+
on_update(state)
|
|
213
363
|
return reviewed
|
|
214
364
|
|
|
215
365
|
|
|
@@ -258,15 +408,25 @@ def _read_key(timeout: float) -> str:
|
|
|
258
408
|
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
259
409
|
|
|
260
410
|
|
|
261
|
-
def build_action_command(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
411
|
+
def build_action_command(
|
|
412
|
+
action: str,
|
|
413
|
+
sha: str,
|
|
414
|
+
*,
|
|
415
|
+
executable: str = "refactor",
|
|
416
|
+
branch: str | None = None,
|
|
417
|
+
rr_id: str | None = None,
|
|
418
|
+
) -> list[str] | None:
|
|
419
|
+
"""Build the argv for a terminal action. Pure + testable (docs/42, docs/43).
|
|
420
|
+
|
|
421
|
+
Review phase (docs/41): ``code`` reuses pending requests; ``refresh``
|
|
422
|
+
regenerates them. Both pass ``--keep`` so resolving from the terminal only
|
|
423
|
+
*builds* the refactor branch and never pops its own merge/keep/drop prompt —
|
|
424
|
+
the finalize phase's ``[m]``/``[x]`` are the single, explicit place that
|
|
425
|
+
decision is made.
|
|
426
|
+
|
|
427
|
+
Finalize phase (docs/43): ``merge``/``drop`` target this commit's refactor
|
|
428
|
+
``branch`` (scoped with ``--branch`` when known so a multi-commit terminal is
|
|
429
|
+
unambiguous); ``revert`` reverses a single ``rr_id``. All are local only.
|
|
270
430
|
"""
|
|
271
431
|
if action == "code":
|
|
272
432
|
# Commit-scoped so `[r]` resolves THIS commit's requests (docs/41 Phase 4).
|
|
@@ -279,10 +439,21 @@ def build_action_command(action: str, sha: str, *, executable: str = "refactor")
|
|
|
279
439
|
return [executable, "diff"]
|
|
280
440
|
if action == "merge":
|
|
281
441
|
# `[m]` merges this commit's refactor branch into your working branch.
|
|
282
|
-
|
|
442
|
+
cmd = [executable, "branch", "merge"]
|
|
443
|
+
if branch:
|
|
444
|
+
cmd += ["--branch", branch]
|
|
445
|
+
return cmd
|
|
283
446
|
if action == "drop":
|
|
284
447
|
# `[x]` discards this commit's refactor branch and its commits.
|
|
285
|
-
|
|
448
|
+
cmd = [executable, "branch", "drop"]
|
|
449
|
+
if branch:
|
|
450
|
+
cmd += ["--branch", branch]
|
|
451
|
+
return cmd
|
|
452
|
+
if action == "revert":
|
|
453
|
+
# `[v]` reverses one resolved request via `git revert` (docs/42).
|
|
454
|
+
if not rr_id:
|
|
455
|
+
return None
|
|
456
|
+
return [executable, "request", "revert", "--id", rr_id]
|
|
286
457
|
return None
|
|
287
458
|
|
|
288
459
|
|
|
@@ -304,7 +475,7 @@ def _run_json_stream(project_root: Path, state: WatchState, runner: ReviewRunner
|
|
|
304
475
|
|
|
305
476
|
def emit(st: WatchState) -> None:
|
|
306
477
|
for node in st.nodes_oldest_first():
|
|
307
|
-
signature = f"{node.state}:{node.gate_status}:{node.rr_count}"
|
|
478
|
+
signature = f"{node.state}:{node.phase}:{node.gate_status}:{node.rr_count}"
|
|
308
479
|
if emitted.get(node.sha) != signature:
|
|
309
480
|
emitted[node.sha] = signature
|
|
310
481
|
sys.stdout.write(json.dumps({"type": "node", "node": node.to_contract()}) + "\n")
|
|
@@ -317,9 +488,25 @@ def _run_json_stream(project_root: Path, state: WatchState, runner: ReviewRunner
|
|
|
317
488
|
time.sleep(_POLL_SECONDS)
|
|
318
489
|
|
|
319
490
|
|
|
320
|
-
def
|
|
491
|
+
def _sync_current_branch(project_root: Path, state: WatchState) -> None:
|
|
492
|
+
try:
|
|
493
|
+
state.current_branch = git_scope.current_branch(project_root) or state.current_branch
|
|
494
|
+
except Exception:
|
|
495
|
+
pass
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _run_action(
|
|
499
|
+
live,
|
|
500
|
+
project_root: Path,
|
|
501
|
+
action: str,
|
|
502
|
+
node,
|
|
503
|
+
state: WatchState,
|
|
504
|
+
*,
|
|
505
|
+
rr_id: str | None = None,
|
|
506
|
+
) -> None:
|
|
321
507
|
"""Suspend the live graph, run an interactive action, then refresh the node."""
|
|
322
|
-
|
|
508
|
+
branch = node.finalize.refactor_branch if getattr(node, "finalize", None) else None
|
|
509
|
+
cmd = build_action_command(action, node.sha, branch=branch, rr_id=rr_id)
|
|
323
510
|
if not cmd:
|
|
324
511
|
return
|
|
325
512
|
live.stop()
|
|
@@ -328,21 +515,134 @@ def _run_action(live, project_root: Path, action: str, node, state: WatchState)
|
|
|
328
515
|
_launch_interactive(cmd, project_root)
|
|
329
516
|
if action == "refresh":
|
|
330
517
|
# `[f]` explicitly regenerates requests, so re-review this commit on the
|
|
331
|
-
# next pass.
|
|
518
|
+
# next pass (also clears any `[b] back` override).
|
|
332
519
|
state.requeue(node.sha)
|
|
333
520
|
elif action == "code":
|
|
334
521
|
# `[r]` is resolve/reuse. Do NOT auto re-review here (that can regenerate
|
|
335
522
|
# duplicate-looking findings); instead reflect current pending requests
|
|
336
|
-
# from the existing run in-place
|
|
523
|
+
# from the existing run in-place, then fold in the refactor branch so the
|
|
524
|
+
# node auto-transitions to the finalize view (docs/43 FR-02).
|
|
525
|
+
state.forced_review.discard(node.sha)
|
|
337
526
|
node_after = state.get(node.sha)
|
|
338
527
|
if node_after and node_after.run_id:
|
|
339
528
|
pending_count, preview = _pending_for_run(project_root, node_after.run_id)
|
|
340
529
|
node_after.rr_count = pending_count
|
|
341
530
|
node_after.rr_preview = preview
|
|
531
|
+
_sync_current_branch(project_root, state)
|
|
532
|
+
refresh_finalize(project_root, state)
|
|
533
|
+
elif action in ("merge", "drop", "revert"):
|
|
534
|
+
# Finalize actions (docs/43). `merge_branch` records the merged outcome
|
|
535
|
+
# itself; a completed drop leaves no branch, so tombstone the dropped
|
|
536
|
+
# outcome here so the node collapses to a DONE (dropped) row.
|
|
537
|
+
if action == "drop" and branch and not git_scope.branch_exists(project_root, branch):
|
|
538
|
+
refactor_branch_store.set_outcome(project_root, branch, refactor_branch_store.OUTCOME_DROPPED)
|
|
539
|
+
_sync_current_branch(project_root, state)
|
|
540
|
+
refresh_finalize(project_root, state)
|
|
541
|
+
console.print("\n[dim]watch: returning to the Refactor Terminal...[/dim]")
|
|
542
|
+
live.start()
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _run_revert_action(live, project_root: Path, node, state: WatchState) -> None:
|
|
546
|
+
"""`[v]`: pick one resolved request and reverse it via `refactor request revert`."""
|
|
547
|
+
fin = getattr(node, "finalize", None)
|
|
548
|
+
commits = [c for c in (fin.commits if fin else []) if not c.reverted]
|
|
549
|
+
if not commits:
|
|
550
|
+
console.print("[dim]watch: no revertible requests on this refactor branch.[/dim]")
|
|
551
|
+
return
|
|
552
|
+
live.stop()
|
|
553
|
+
console.print("\n[bold bright_cyan]watch:[/bold bright_cyan] revert a refactor request "
|
|
554
|
+
f"on [cyan]{fin.refactor_branch}[/cyan]\n")
|
|
555
|
+
for idx, commit in enumerate(commits, start=1):
|
|
556
|
+
console.print(
|
|
557
|
+
f" [bold]{idx}[/bold]. {commit.location or '(unknown)'} "
|
|
558
|
+
f"[dim](rr {commit.rr_id[:7]}, {(commit.commit_sha or '')[:8]})[/dim]"
|
|
559
|
+
)
|
|
560
|
+
try:
|
|
561
|
+
raw = typer.prompt("Number to revert (blank to cancel)", default="")
|
|
562
|
+
except (typer.Abort, EOFError, KeyboardInterrupt):
|
|
563
|
+
raw = ""
|
|
564
|
+
choice = str(raw).strip()
|
|
565
|
+
if choice:
|
|
566
|
+
try:
|
|
567
|
+
idx = int(choice)
|
|
568
|
+
except ValueError:
|
|
569
|
+
idx = 0
|
|
570
|
+
if 1 <= idx <= len(commits):
|
|
571
|
+
rr_id = commits[idx - 1].rr_id
|
|
572
|
+
_launch_interactive(
|
|
573
|
+
build_action_command("revert", node.sha, rr_id=rr_id), project_root
|
|
574
|
+
)
|
|
575
|
+
refresh_finalize(project_root, state)
|
|
576
|
+
else:
|
|
577
|
+
console.print("[dim]watch: nothing reverted (out of range).[/dim]")
|
|
578
|
+
else:
|
|
579
|
+
console.print("[dim]watch: revert cancelled.[/dim]")
|
|
342
580
|
console.print("\n[dim]watch: returning to the Refactor Terminal...[/dim]")
|
|
343
581
|
live.start()
|
|
344
582
|
|
|
345
583
|
|
|
584
|
+
def _dispatch_key(live, project_root: Path, key: str, state: WatchState) -> None:
|
|
585
|
+
"""Route a hotkey to the right action for the selected node's phase (docs/43).
|
|
586
|
+
|
|
587
|
+
Finalize phase offers ``[m]/[x]/[d]/[v]/[b]`` (review keys are hidden);
|
|
588
|
+
review/none/done offers ``[r]/[f]/[d]`` (and ``[b]`` toggles back to a
|
|
589
|
+
finalize node the user pressed ``[b]`` on).
|
|
590
|
+
"""
|
|
591
|
+
node = state.selected()
|
|
592
|
+
if node is None:
|
|
593
|
+
return
|
|
594
|
+
|
|
595
|
+
if state.show_finalize(node):
|
|
596
|
+
# --- finalize phase: merge / drop / diff / revert / back -------------- #
|
|
597
|
+
if key == "m":
|
|
598
|
+
_run_action(live, project_root, "merge", node, state)
|
|
599
|
+
elif key == "x":
|
|
600
|
+
_run_action(live, project_root, "drop", node, state)
|
|
601
|
+
elif key == "d":
|
|
602
|
+
_run_action(live, project_root, "diff", node, state)
|
|
603
|
+
elif key == "v":
|
|
604
|
+
_run_revert_action(live, project_root, node, state)
|
|
605
|
+
elif key == "b":
|
|
606
|
+
# Show the review view for this commit so `[f]` fresh review is possible.
|
|
607
|
+
state.forced_review.add(node.sha)
|
|
608
|
+
elif key in ("r", "f"):
|
|
609
|
+
console.print(
|
|
610
|
+
"[dim]watch: this commit is resolved; press [b] to go back and "
|
|
611
|
+
"re-review, or [m] to merge.[/dim]"
|
|
612
|
+
)
|
|
613
|
+
return
|
|
614
|
+
|
|
615
|
+
# --- review / none / done phase ----------------------------------------- #
|
|
616
|
+
if key == "b" and node.sha in state.forced_review:
|
|
617
|
+
# Toggle back to the finalize view for a node we forced into review.
|
|
618
|
+
state.forced_review.discard(node.sha)
|
|
619
|
+
return
|
|
620
|
+
if key in ("r", "f", "d"):
|
|
621
|
+
can_review = node.phase == watch_state.PHASE_REVIEW or node.sha in state.forced_review
|
|
622
|
+
if key == "d":
|
|
623
|
+
if node.state == watch_state.REVIEWED:
|
|
624
|
+
_run_action(live, project_root, "diff", node, state)
|
|
625
|
+
return
|
|
626
|
+
if not can_review:
|
|
627
|
+
console.print(
|
|
628
|
+
f"[dim]watch: nothing to review on {node.short_sha} "
|
|
629
|
+
f"(phase: {node.phase}).[/dim]"
|
|
630
|
+
)
|
|
631
|
+
return
|
|
632
|
+
if node.state == watch_state.REVIEWED:
|
|
633
|
+
_run_action(live, project_root, "code" if key == "r" else "refresh", node, state)
|
|
634
|
+
else:
|
|
635
|
+
console.print(
|
|
636
|
+
f"[dim]watch: {node.short_sha} is '{node.state}'; "
|
|
637
|
+
"wait for review to finish before acting.[/dim]"
|
|
638
|
+
)
|
|
639
|
+
elif key in ("m", "x", "v"):
|
|
640
|
+
console.print(
|
|
641
|
+
"[dim]watch: finalize actions apply after this commit's requests are "
|
|
642
|
+
"resolved onto a refactor branch (press [r]).[/dim]"
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
|
|
346
646
|
def _run_tui(project_root: Path, state: WatchState, runner: ReviewRunner) -> None:
|
|
347
647
|
"""Render the live Graphite-style graph and consume the queue serially."""
|
|
348
648
|
from refactorai_cli.main import _print_agent_intro
|
|
@@ -370,26 +670,12 @@ def _run_tui(project_root: Path, state: WatchState, runner: ReviewRunner) -> Non
|
|
|
370
670
|
state.move_selection(-1)
|
|
371
671
|
elif key in ("down", "j"):
|
|
372
672
|
state.move_selection(+1)
|
|
373
|
-
elif key in ("r", "f", "d", "m", "x"):
|
|
374
|
-
action = {
|
|
375
|
-
"r": "code",
|
|
376
|
-
"f": "refresh",
|
|
377
|
-
"d": "diff",
|
|
378
|
-
"m": "merge",
|
|
379
|
-
"x": "drop",
|
|
380
|
-
}[key]
|
|
381
|
-
node = state.selected()
|
|
382
|
-
if node and node.state == watch_state.REVIEWED:
|
|
383
|
-
_run_action(live, project_root, action, node, state)
|
|
384
|
-
elif node:
|
|
385
|
-
console.print(
|
|
386
|
-
f"[dim]watch: {node.short_sha} is '{node.state}'; "
|
|
387
|
-
"wait for review to finish before acting.[/dim]"
|
|
388
|
-
)
|
|
389
673
|
elif key == "p":
|
|
390
674
|
console.print(
|
|
391
675
|
"[dim]watch: push arrives in Phase 5; use `git push` for now.[/dim]"
|
|
392
676
|
)
|
|
677
|
+
elif key in ("r", "f", "d", "m", "x", "v", "b"):
|
|
678
|
+
_dispatch_key(live, project_root, key, state)
|
|
393
679
|
refresh(state)
|
|
394
680
|
finally:
|
|
395
681
|
live.stop()
|
|
@@ -418,6 +704,10 @@ def watch(
|
|
|
418
704
|
_ensure_auth_in_window(json_output)
|
|
419
705
|
|
|
420
706
|
state = WatchState(repo_name=project_root.name)
|
|
707
|
+
try:
|
|
708
|
+
state.current_branch = git_scope.current_branch(project_root) or ""
|
|
709
|
+
except Exception:
|
|
710
|
+
state.current_branch = ""
|
|
421
711
|
for event in _load_initial_events(project_root, since, limit):
|
|
422
712
|
sha = str(event.get("sha") or "")
|
|
423
713
|
if sha and not state.has(sha):
|