refactorai-cli 0.4.9__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.9 → refactorai_cli-0.4.11}/PKG-INFO +2 -2
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/pyproject.toml +2 -2
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/__init__.py +1 -1
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commands/branch_cmds.py +3 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commands/run_cmds.py +10 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commands/watch_cmds.py +326 -32
- refactorai_cli-0.4.11/refactorai_cli/refactor_branch_store.py +189 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/watch_state.py +289 -13
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli.egg-info/PKG-INFO +2 -2
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli.egg-info/requires.txt +1 -1
- refactorai_cli-0.4.9/refactorai_cli/refactor_branch_store.py +0 -116
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/README.md +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/auth.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/client.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/cloud_rr.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commands/__init__.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commands/account_cmds.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commands/auth_cmds.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commands/cloud_cmds.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commands/engine_cmds.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commands/hook_cmds.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commands/model_cmds.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commands/request_cmds.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commands/rules_cmds.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commands/runtime_cmds.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commands/setup_cmds.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commands/toolchains_cmds.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/commit_queue.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/control_plane.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/credentials.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/dotenv_loader.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/git_scope.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/local_constitution.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/local_engine_runtime.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/local_paths.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/main.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/model_policy.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/review_runner.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/runtime_manager.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/settings.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli/setup_flow.py +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli.egg-info/SOURCES.txt +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli.egg-info/dependency_links.txt +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli.egg-info/entry_points.txt +0 -0
- {refactorai_cli-0.4.9 → refactorai_cli-0.4.11}/refactorai_cli.egg-info/top_level.txt +0 -0
- {refactorai_cli-0.4.9 → 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,27 +408,52 @@ 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
|
-
|
|
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.
|
|
266
430
|
"""
|
|
267
431
|
if action == "code":
|
|
268
432
|
# Commit-scoped so `[r]` resolves THIS commit's requests (docs/41 Phase 4).
|
|
269
|
-
return [executable, "code", "--commit", sha]
|
|
433
|
+
return [executable, "code", "--commit", sha, "--keep"]
|
|
270
434
|
if action == "refresh":
|
|
271
435
|
# `[f]` regenerates the requests for this commit before resolving.
|
|
272
|
-
return [executable, "code", "--commit", sha, "--refresh-requests"]
|
|
436
|
+
return [executable, "code", "--commit", sha, "--refresh-requests", "--keep"]
|
|
273
437
|
if action == "diff":
|
|
274
438
|
# Shows the pending refactor-request patch diffs (not the git diff).
|
|
275
439
|
return [executable, "diff"]
|
|
276
440
|
if action == "merge":
|
|
277
441
|
# `[m]` merges this commit's refactor branch into your working branch.
|
|
278
|
-
|
|
442
|
+
cmd = [executable, "branch", "merge"]
|
|
443
|
+
if branch:
|
|
444
|
+
cmd += ["--branch", branch]
|
|
445
|
+
return cmd
|
|
279
446
|
if action == "drop":
|
|
280
447
|
# `[x]` discards this commit's refactor branch and its commits.
|
|
281
|
-
|
|
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]
|
|
282
457
|
return None
|
|
283
458
|
|
|
284
459
|
|
|
@@ -300,7 +475,7 @@ def _run_json_stream(project_root: Path, state: WatchState, runner: ReviewRunner
|
|
|
300
475
|
|
|
301
476
|
def emit(st: WatchState) -> None:
|
|
302
477
|
for node in st.nodes_oldest_first():
|
|
303
|
-
signature = f"{node.state}:{node.gate_status}:{node.rr_count}"
|
|
478
|
+
signature = f"{node.state}:{node.phase}:{node.gate_status}:{node.rr_count}"
|
|
304
479
|
if emitted.get(node.sha) != signature:
|
|
305
480
|
emitted[node.sha] = signature
|
|
306
481
|
sys.stdout.write(json.dumps({"type": "node", "node": node.to_contract()}) + "\n")
|
|
@@ -313,9 +488,25 @@ def _run_json_stream(project_root: Path, state: WatchState, runner: ReviewRunner
|
|
|
313
488
|
time.sleep(_POLL_SECONDS)
|
|
314
489
|
|
|
315
490
|
|
|
316
|
-
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:
|
|
317
507
|
"""Suspend the live graph, run an interactive action, then refresh the node."""
|
|
318
|
-
|
|
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)
|
|
319
510
|
if not cmd:
|
|
320
511
|
return
|
|
321
512
|
live.stop()
|
|
@@ -324,21 +515,134 @@ def _run_action(live, project_root: Path, action: str, node, state: WatchState)
|
|
|
324
515
|
_launch_interactive(cmd, project_root)
|
|
325
516
|
if action == "refresh":
|
|
326
517
|
# `[f]` explicitly regenerates requests, so re-review this commit on the
|
|
327
|
-
# next pass.
|
|
518
|
+
# next pass (also clears any `[b] back` override).
|
|
328
519
|
state.requeue(node.sha)
|
|
329
520
|
elif action == "code":
|
|
330
521
|
# `[r]` is resolve/reuse. Do NOT auto re-review here (that can regenerate
|
|
331
522
|
# duplicate-looking findings); instead reflect current pending requests
|
|
332
|
-
# 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)
|
|
333
526
|
node_after = state.get(node.sha)
|
|
334
527
|
if node_after and node_after.run_id:
|
|
335
528
|
pending_count, preview = _pending_for_run(project_root, node_after.run_id)
|
|
336
529
|
node_after.rr_count = pending_count
|
|
337
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]")
|
|
338
580
|
console.print("\n[dim]watch: returning to the Refactor Terminal...[/dim]")
|
|
339
581
|
live.start()
|
|
340
582
|
|
|
341
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
|
+
|
|
342
646
|
def _run_tui(project_root: Path, state: WatchState, runner: ReviewRunner) -> None:
|
|
343
647
|
"""Render the live Graphite-style graph and consume the queue serially."""
|
|
344
648
|
from refactorai_cli.main import _print_agent_intro
|
|
@@ -366,26 +670,12 @@ def _run_tui(project_root: Path, state: WatchState, runner: ReviewRunner) -> Non
|
|
|
366
670
|
state.move_selection(-1)
|
|
367
671
|
elif key in ("down", "j"):
|
|
368
672
|
state.move_selection(+1)
|
|
369
|
-
elif key in ("r", "f", "d", "m", "x"):
|
|
370
|
-
action = {
|
|
371
|
-
"r": "code",
|
|
372
|
-
"f": "refresh",
|
|
373
|
-
"d": "diff",
|
|
374
|
-
"m": "merge",
|
|
375
|
-
"x": "drop",
|
|
376
|
-
}[key]
|
|
377
|
-
node = state.selected()
|
|
378
|
-
if node and node.state == watch_state.REVIEWED:
|
|
379
|
-
_run_action(live, project_root, action, node, state)
|
|
380
|
-
elif node:
|
|
381
|
-
console.print(
|
|
382
|
-
f"[dim]watch: {node.short_sha} is '{node.state}'; "
|
|
383
|
-
"wait for review to finish before acting.[/dim]"
|
|
384
|
-
)
|
|
385
673
|
elif key == "p":
|
|
386
674
|
console.print(
|
|
387
675
|
"[dim]watch: push arrives in Phase 5; use `git push` for now.[/dim]"
|
|
388
676
|
)
|
|
677
|
+
elif key in ("r", "f", "d", "m", "x", "v", "b"):
|
|
678
|
+
_dispatch_key(live, project_root, key, state)
|
|
389
679
|
refresh(state)
|
|
390
680
|
finally:
|
|
391
681
|
live.stop()
|
|
@@ -414,6 +704,10 @@ def watch(
|
|
|
414
704
|
_ensure_auth_in_window(json_output)
|
|
415
705
|
|
|
416
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 = ""
|
|
417
711
|
for event in _load_initial_events(project_root, since, limit):
|
|
418
712
|
sha = str(event.get("sha") or "")
|
|
419
713
|
if sha and not state.has(sha):
|