refactorai-cli 0.4.10__tar.gz → 0.4.12__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.12}/PKG-INFO +2 -2
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/pyproject.toml +2 -2
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/__init__.py +1 -1
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/cloud_rr.py +15 -2
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commands/branch_cmds.py +3 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commands/run_cmds.py +27 -6
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commands/watch_cmds.py +340 -36
- refactorai_cli-0.4.12/refactorai_cli/refactor_branch_store.py +189 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/watch_state.py +289 -13
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli.egg-info/PKG-INFO +2 -2
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/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.12}/README.md +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/auth.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/client.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commands/__init__.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commands/account_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commands/auth_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commands/cloud_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commands/engine_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commands/hook_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commands/model_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commands/request_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commands/rules_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commands/runtime_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commands/runtime_proxy_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commands/setup_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commands/toolchains_cmds.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/commit_queue.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/control_plane.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/credentials.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/dotenv_loader.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/git_scope.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/local_constitution.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/local_engine_runtime.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/local_paths.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/main.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/model_policy.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/review_runner.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/runtime_manager.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/settings.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli/setup_flow.py +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli.egg-info/SOURCES.txt +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli.egg-info/dependency_links.txt +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli.egg-info/entry_points.txt +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/refactorai_cli.egg-info/top_level.txt +0 -0
- {refactorai_cli-0.4.10 → refactorai_cli-0.4.12}/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.12
|
|
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.7
|
|
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.12"
|
|
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.7",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
[project.scripts]
|
|
@@ -651,6 +651,7 @@ def run_cloud_refactor_requests(
|
|
|
651
651
|
apply_byok_payload: Callable[[dict, object], None] | None = None,
|
|
652
652
|
toolchain_consent_cb: Callable[[dict], bool] | None = None,
|
|
653
653
|
commit_cb: Callable[[object], None] | None = None,
|
|
654
|
+
on_rr_failure: str = "continue",
|
|
654
655
|
) -> RunArtifact:
|
|
655
656
|
"""Drive pending RRs through the cloud server-run loop and return a summary.
|
|
656
657
|
|
|
@@ -665,6 +666,12 @@ def run_cloud_refactor_requests(
|
|
|
665
666
|
once per RR that reaches ``APPLIED`` so the CLI can record one git commit per
|
|
666
667
|
resolved request on an isolated refactor branch. Best-effort; git failures
|
|
667
668
|
never break the cloud RR loop.
|
|
669
|
+
|
|
670
|
+
``on_rr_failure`` (``continue`` | ``defer`` | ``stop``) mirrors the local
|
|
671
|
+
executor policy and is forwarded to the server so a single failing RR no
|
|
672
|
+
longer halts the whole run by default (parity with local). The client stays a
|
|
673
|
+
thin executor: it stops only when the server returns a ``failed`` verdict with
|
|
674
|
+
no ``next_rr_id`` (older servers that always stop remain compatible).
|
|
668
675
|
"""
|
|
669
676
|
project_root = Path(project_root)
|
|
670
677
|
progress_cb = progress_cb or (lambda e: None)
|
|
@@ -741,6 +748,7 @@ def run_cloud_refactor_requests(
|
|
|
741
748
|
"constitution_hash": constitution.content_hash,
|
|
742
749
|
"constitution_body": getattr(constitution, "body", "") or "",
|
|
743
750
|
"model_hint": resolved_model,
|
|
751
|
+
"on_rr_failure": str(on_rr_failure or "continue"),
|
|
744
752
|
"rrs": [
|
|
745
753
|
{
|
|
746
754
|
"rr_id": rr.rr_id,
|
|
@@ -875,12 +883,17 @@ def run_cloud_refactor_requests(
|
|
|
875
883
|
attempts_by_id.get(completed_id, 0 if verdict == "deferred" else 1),
|
|
876
884
|
outcomes, counts, progress_cb, commit_cb=commit_cb,
|
|
877
885
|
)
|
|
878
|
-
|
|
886
|
+
next_id = str(action.get("next_rr_id", "") or "")
|
|
887
|
+
# With `continue`/`defer` (the default) the server advances after a
|
|
888
|
+
# failure, so the client resolves the remaining requests in one pass
|
|
889
|
+
# instead of stopping at the first failure (parity with local). Only a
|
|
890
|
+
# `stop` policy halts the run (no next RR named); a failed *last* RR
|
|
891
|
+
# under `continue` simply ends the queue, it is not a halt.
|
|
892
|
+
if verdict == "failed" and not next_id and str(on_rr_failure) == "stop":
|
|
879
893
|
stopped = True
|
|
880
894
|
stop_reason = str(action.get("message", "") or "refactor request failed")
|
|
881
895
|
_emit(progress_cb, phase="rr", event="run_stopped", rr_id=completed_id, reason=stop_reason)
|
|
882
896
|
break
|
|
883
|
-
next_id = str(action.get("next_rr_id", "") or "")
|
|
884
897
|
if not next_id:
|
|
885
898
|
break
|
|
886
899
|
current_rr_id = next_id
|
|
@@ -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:
|
|
@@ -2955,7 +2955,19 @@ def code(
|
|
|
2955
2955
|
changed=changed,
|
|
2956
2956
|
intent=intent,
|
|
2957
2957
|
)
|
|
2958
|
-
|
|
2958
|
+
# Decide reuse-vs-generate up front. The full-project confirmation only guards
|
|
2959
|
+
# *creating* a large request set; when we are resolving an existing pending
|
|
2960
|
+
# queue (the common `[r]` resume case) there is nothing to confirm, so a root
|
|
2961
|
+
# commit no longer re-prompts every run (docs/42 `R29`, Part L).
|
|
2962
|
+
reused_requests = [] if (code_scope and refresh_requests) else pending_requests(project_root)
|
|
2963
|
+
will_generate_requests = bool(refresh_requests) or not reused_requests
|
|
2964
|
+
if will_generate_requests:
|
|
2965
|
+
_confirm_full_project_scope(code_scope, command="code", assume_yes=yes_full_project)
|
|
2966
|
+
elif code_scope and code_scope.get("full_project"):
|
|
2967
|
+
console.print(
|
|
2968
|
+
f"[dim]code: resuming {len(reused_requests)} pending request(s) for this "
|
|
2969
|
+
"commit (full-project review already ran).[/dim]"
|
|
2970
|
+
)
|
|
2959
2971
|
code_scope_paths = code_scope.get("scope_paths") if code_scope else None
|
|
2960
2972
|
code_changed_paths = code_scope.get("changed_paths") if code_scope else None
|
|
2961
2973
|
if code_scope:
|
|
@@ -3043,11 +3055,9 @@ def code(
|
|
|
3043
3055
|
# Reuse the existing pending requests by default (docs/42 `R29`, Phase 1):
|
|
3044
3056
|
# `[r]`/`code --commit` resolves the queue already created for this commit
|
|
3045
3057
|
# rather than regenerating it. `--refresh-requests` opts back into a fresh
|
|
3046
|
-
# scoped review so the request set is rebuilt from scratch.
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
else:
|
|
3050
|
-
requests = pending_requests(project_root)
|
|
3058
|
+
# scoped review so the request set is rebuilt from scratch. Computed above
|
|
3059
|
+
# (``reused_requests``) so the full-project prompt could be skipped on reuse.
|
|
3060
|
+
requests = reused_requests
|
|
3051
3061
|
if not requests:
|
|
3052
3062
|
if code_scope and refresh_requests:
|
|
3053
3063
|
console.print(
|
|
@@ -3169,6 +3179,7 @@ def code(
|
|
|
3169
3179
|
apply_byok_payload=_apply_byok_payload,
|
|
3170
3180
|
toolchain_consent_cb=_toolchain_consent_prompt(auto_approve=auto_approve),
|
|
3171
3181
|
commit_cb=rr_commit_cb,
|
|
3182
|
+
on_rr_failure=on_rr_failure,
|
|
3172
3183
|
)
|
|
3173
3184
|
else:
|
|
3174
3185
|
# Local mode is unchanged: dispatch to the local executor with the
|
|
@@ -3217,6 +3228,16 @@ def code(
|
|
|
3217
3228
|
final_status = "skipped"
|
|
3218
3229
|
if _final_test_enabled(project_config):
|
|
3219
3230
|
final_status, _ = _run_final_test_suite(project_root, project_config)
|
|
3231
|
+
# docs/43 `R30`: persist the verdict so the Refactor Terminal finalize view
|
|
3232
|
+
# can show it without re-running the suite.
|
|
3233
|
+
try:
|
|
3234
|
+
from refactorai_cli import refactor_branch_store
|
|
3235
|
+
|
|
3236
|
+
refactor_branch_store.set_final_test(
|
|
3237
|
+
project_root, refactor_branch_ctx.get("branch_name", ""), final_status
|
|
3238
|
+
)
|
|
3239
|
+
except Exception:
|
|
3240
|
+
pass
|
|
3220
3241
|
decision = _decide_finalize(
|
|
3221
3242
|
project_config,
|
|
3222
3243
|
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,31 +408,61 @@ 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
|
+
yes_full_project: bool = False,
|
|
419
|
+
) -> list[str] | None:
|
|
420
|
+
"""Build the argv for a terminal action. Pure + testable (docs/42, docs/43).
|
|
421
|
+
|
|
422
|
+
Review phase (docs/41): ``code`` reuses pending requests; ``refresh``
|
|
423
|
+
regenerates them. Both pass ``--keep`` so resolving from the terminal only
|
|
424
|
+
*builds* the refactor branch and never pops its own merge/keep/drop prompt —
|
|
425
|
+
the finalize phase's ``[m]``/``[x]`` are the single, explicit place that
|
|
426
|
+
decision is made.
|
|
427
|
+
|
|
428
|
+
Finalize phase (docs/43): ``merge``/``drop`` target this commit's refactor
|
|
429
|
+
``branch`` (scoped with ``--branch`` when known so a multi-commit terminal is
|
|
430
|
+
unambiguous); ``revert`` reverses a single ``rr_id``. All are local only.
|
|
270
431
|
"""
|
|
271
432
|
if action == "code":
|
|
272
433
|
# Commit-scoped so `[r]` resolves THIS commit's requests (docs/41 Phase 4).
|
|
273
|
-
|
|
434
|
+
cmd = [executable, "code", "--commit", sha, "--keep"]
|
|
435
|
+
if yes_full_project:
|
|
436
|
+
# Root/full-scope commits would otherwise pop a hidden confirmation in
|
|
437
|
+
# the suspended terminal; auto-confirm so `[r]` never appears to hang.
|
|
438
|
+
cmd.append("--yes-full-project")
|
|
439
|
+
return cmd
|
|
274
440
|
if action == "refresh":
|
|
275
441
|
# `[f]` regenerates the requests for this commit before resolving.
|
|
276
|
-
|
|
442
|
+
cmd = [executable, "code", "--commit", sha, "--refresh-requests", "--keep"]
|
|
443
|
+
if yes_full_project:
|
|
444
|
+
cmd.append("--yes-full-project")
|
|
445
|
+
return cmd
|
|
277
446
|
if action == "diff":
|
|
278
447
|
# Shows the pending refactor-request patch diffs (not the git diff).
|
|
279
448
|
return [executable, "diff"]
|
|
280
449
|
if action == "merge":
|
|
281
450
|
# `[m]` merges this commit's refactor branch into your working branch.
|
|
282
|
-
|
|
451
|
+
cmd = [executable, "branch", "merge"]
|
|
452
|
+
if branch:
|
|
453
|
+
cmd += ["--branch", branch]
|
|
454
|
+
return cmd
|
|
283
455
|
if action == "drop":
|
|
284
456
|
# `[x]` discards this commit's refactor branch and its commits.
|
|
285
|
-
|
|
457
|
+
cmd = [executable, "branch", "drop"]
|
|
458
|
+
if branch:
|
|
459
|
+
cmd += ["--branch", branch]
|
|
460
|
+
return cmd
|
|
461
|
+
if action == "revert":
|
|
462
|
+
# `[v]` reverses one resolved request via `git revert` (docs/42).
|
|
463
|
+
if not rr_id:
|
|
464
|
+
return None
|
|
465
|
+
return [executable, "request", "revert", "--id", rr_id]
|
|
286
466
|
return None
|
|
287
467
|
|
|
288
468
|
|
|
@@ -304,7 +484,7 @@ def _run_json_stream(project_root: Path, state: WatchState, runner: ReviewRunner
|
|
|
304
484
|
|
|
305
485
|
def emit(st: WatchState) -> None:
|
|
306
486
|
for node in st.nodes_oldest_first():
|
|
307
|
-
signature = f"{node.state}:{node.gate_status}:{node.rr_count}"
|
|
487
|
+
signature = f"{node.state}:{node.phase}:{node.gate_status}:{node.rr_count}"
|
|
308
488
|
if emitted.get(node.sha) != signature:
|
|
309
489
|
emitted[node.sha] = signature
|
|
310
490
|
sys.stdout.write(json.dumps({"type": "node", "node": node.to_contract()}) + "\n")
|
|
@@ -317,9 +497,30 @@ def _run_json_stream(project_root: Path, state: WatchState, runner: ReviewRunner
|
|
|
317
497
|
time.sleep(_POLL_SECONDS)
|
|
318
498
|
|
|
319
499
|
|
|
320
|
-
def
|
|
500
|
+
def _sync_current_branch(project_root: Path, state: WatchState) -> None:
|
|
501
|
+
try:
|
|
502
|
+
state.current_branch = git_scope.current_branch(project_root) or state.current_branch
|
|
503
|
+
except Exception:
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _run_action(
|
|
508
|
+
live,
|
|
509
|
+
project_root: Path,
|
|
510
|
+
action: str,
|
|
511
|
+
node,
|
|
512
|
+
state: WatchState,
|
|
513
|
+
*,
|
|
514
|
+
rr_id: str | None = None,
|
|
515
|
+
) -> None:
|
|
321
516
|
"""Suspend the live graph, run an interactive action, then refresh the node."""
|
|
322
|
-
|
|
517
|
+
branch = node.finalize.refactor_branch if getattr(node, "finalize", None) else None
|
|
518
|
+
# Root/full-scope commits expand `code` to a full-project review; auto-confirm
|
|
519
|
+
# so `[r]`/`[f]` never stall on a hidden prompt in the suspended terminal.
|
|
520
|
+
yes_full_project = getattr(node, "scope_mode", "") == "full"
|
|
521
|
+
cmd = build_action_command(
|
|
522
|
+
action, node.sha, branch=branch, rr_id=rr_id, yes_full_project=yes_full_project
|
|
523
|
+
)
|
|
323
524
|
if not cmd:
|
|
324
525
|
return
|
|
325
526
|
live.stop()
|
|
@@ -328,21 +529,134 @@ def _run_action(live, project_root: Path, action: str, node, state: WatchState)
|
|
|
328
529
|
_launch_interactive(cmd, project_root)
|
|
329
530
|
if action == "refresh":
|
|
330
531
|
# `[f]` explicitly regenerates requests, so re-review this commit on the
|
|
331
|
-
# next pass.
|
|
532
|
+
# next pass (also clears any `[b] back` override).
|
|
332
533
|
state.requeue(node.sha)
|
|
333
534
|
elif action == "code":
|
|
334
535
|
# `[r]` is resolve/reuse. Do NOT auto re-review here (that can regenerate
|
|
335
536
|
# duplicate-looking findings); instead reflect current pending requests
|
|
336
|
-
# from the existing run in-place
|
|
537
|
+
# from the existing run in-place, then fold in the refactor branch so the
|
|
538
|
+
# node auto-transitions to the finalize view (docs/43 FR-02).
|
|
539
|
+
state.forced_review.discard(node.sha)
|
|
337
540
|
node_after = state.get(node.sha)
|
|
338
541
|
if node_after and node_after.run_id:
|
|
339
542
|
pending_count, preview = _pending_for_run(project_root, node_after.run_id)
|
|
340
543
|
node_after.rr_count = pending_count
|
|
341
544
|
node_after.rr_preview = preview
|
|
545
|
+
_sync_current_branch(project_root, state)
|
|
546
|
+
refresh_finalize(project_root, state)
|
|
547
|
+
elif action in ("merge", "drop", "revert"):
|
|
548
|
+
# Finalize actions (docs/43). `merge_branch` records the merged outcome
|
|
549
|
+
# itself; a completed drop leaves no branch, so tombstone the dropped
|
|
550
|
+
# outcome here so the node collapses to a DONE (dropped) row.
|
|
551
|
+
if action == "drop" and branch and not git_scope.branch_exists(project_root, branch):
|
|
552
|
+
refactor_branch_store.set_outcome(project_root, branch, refactor_branch_store.OUTCOME_DROPPED)
|
|
553
|
+
_sync_current_branch(project_root, state)
|
|
554
|
+
refresh_finalize(project_root, state)
|
|
555
|
+
console.print("\n[dim]watch: returning to the Refactor Terminal...[/dim]")
|
|
556
|
+
live.start()
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _run_revert_action(live, project_root: Path, node, state: WatchState) -> None:
|
|
560
|
+
"""`[v]`: pick one resolved request and reverse it via `refactor request revert`."""
|
|
561
|
+
fin = getattr(node, "finalize", None)
|
|
562
|
+
commits = [c for c in (fin.commits if fin else []) if not c.reverted]
|
|
563
|
+
if not commits:
|
|
564
|
+
console.print("[dim]watch: no revertible requests on this refactor branch.[/dim]")
|
|
565
|
+
return
|
|
566
|
+
live.stop()
|
|
567
|
+
console.print("\n[bold bright_cyan]watch:[/bold bright_cyan] revert a refactor request "
|
|
568
|
+
f"on [cyan]{fin.refactor_branch}[/cyan]\n")
|
|
569
|
+
for idx, commit in enumerate(commits, start=1):
|
|
570
|
+
console.print(
|
|
571
|
+
f" [bold]{idx}[/bold]. {commit.location or '(unknown)'} "
|
|
572
|
+
f"[dim](rr {commit.rr_id[:7]}, {(commit.commit_sha or '')[:8]})[/dim]"
|
|
573
|
+
)
|
|
574
|
+
try:
|
|
575
|
+
raw = typer.prompt("Number to revert (blank to cancel)", default="")
|
|
576
|
+
except (typer.Abort, EOFError, KeyboardInterrupt):
|
|
577
|
+
raw = ""
|
|
578
|
+
choice = str(raw).strip()
|
|
579
|
+
if choice:
|
|
580
|
+
try:
|
|
581
|
+
idx = int(choice)
|
|
582
|
+
except ValueError:
|
|
583
|
+
idx = 0
|
|
584
|
+
if 1 <= idx <= len(commits):
|
|
585
|
+
rr_id = commits[idx - 1].rr_id
|
|
586
|
+
_launch_interactive(
|
|
587
|
+
build_action_command("revert", node.sha, rr_id=rr_id), project_root
|
|
588
|
+
)
|
|
589
|
+
refresh_finalize(project_root, state)
|
|
590
|
+
else:
|
|
591
|
+
console.print("[dim]watch: nothing reverted (out of range).[/dim]")
|
|
592
|
+
else:
|
|
593
|
+
console.print("[dim]watch: revert cancelled.[/dim]")
|
|
342
594
|
console.print("\n[dim]watch: returning to the Refactor Terminal...[/dim]")
|
|
343
595
|
live.start()
|
|
344
596
|
|
|
345
597
|
|
|
598
|
+
def _dispatch_key(live, project_root: Path, key: str, state: WatchState) -> None:
|
|
599
|
+
"""Route a hotkey to the right action for the selected node's phase (docs/43).
|
|
600
|
+
|
|
601
|
+
Finalize phase offers ``[m]/[x]/[d]/[v]/[b]`` (review keys are hidden);
|
|
602
|
+
review/none/done offers ``[r]/[f]/[d]`` (and ``[b]`` toggles back to a
|
|
603
|
+
finalize node the user pressed ``[b]`` on).
|
|
604
|
+
"""
|
|
605
|
+
node = state.selected()
|
|
606
|
+
if node is None:
|
|
607
|
+
return
|
|
608
|
+
|
|
609
|
+
if state.show_finalize(node):
|
|
610
|
+
# --- finalize phase: merge / drop / diff / revert / back -------------- #
|
|
611
|
+
if key == "m":
|
|
612
|
+
_run_action(live, project_root, "merge", node, state)
|
|
613
|
+
elif key == "x":
|
|
614
|
+
_run_action(live, project_root, "drop", node, state)
|
|
615
|
+
elif key == "d":
|
|
616
|
+
_run_action(live, project_root, "diff", node, state)
|
|
617
|
+
elif key == "v":
|
|
618
|
+
_run_revert_action(live, project_root, node, state)
|
|
619
|
+
elif key == "b":
|
|
620
|
+
# Show the review view for this commit so `[f]` fresh review is possible.
|
|
621
|
+
state.forced_review.add(node.sha)
|
|
622
|
+
elif key in ("r", "f"):
|
|
623
|
+
console.print(
|
|
624
|
+
"[dim]watch: this commit is resolved; press [b] to go back and "
|
|
625
|
+
"re-review, or [m] to merge.[/dim]"
|
|
626
|
+
)
|
|
627
|
+
return
|
|
628
|
+
|
|
629
|
+
# --- review / none / done phase ----------------------------------------- #
|
|
630
|
+
if key == "b" and node.sha in state.forced_review:
|
|
631
|
+
# Toggle back to the finalize view for a node we forced into review.
|
|
632
|
+
state.forced_review.discard(node.sha)
|
|
633
|
+
return
|
|
634
|
+
if key in ("r", "f", "d"):
|
|
635
|
+
can_review = node.phase == watch_state.PHASE_REVIEW or node.sha in state.forced_review
|
|
636
|
+
if key == "d":
|
|
637
|
+
if node.state == watch_state.REVIEWED:
|
|
638
|
+
_run_action(live, project_root, "diff", node, state)
|
|
639
|
+
return
|
|
640
|
+
if not can_review:
|
|
641
|
+
console.print(
|
|
642
|
+
f"[dim]watch: nothing to review on {node.short_sha} "
|
|
643
|
+
f"(phase: {node.phase}).[/dim]"
|
|
644
|
+
)
|
|
645
|
+
return
|
|
646
|
+
if node.state == watch_state.REVIEWED:
|
|
647
|
+
_run_action(live, project_root, "code" if key == "r" else "refresh", node, state)
|
|
648
|
+
else:
|
|
649
|
+
console.print(
|
|
650
|
+
f"[dim]watch: {node.short_sha} is '{node.state}'; "
|
|
651
|
+
"wait for review to finish before acting.[/dim]"
|
|
652
|
+
)
|
|
653
|
+
elif key in ("m", "x", "v"):
|
|
654
|
+
console.print(
|
|
655
|
+
"[dim]watch: finalize actions apply after this commit's requests are "
|
|
656
|
+
"resolved onto a refactor branch (press [r]).[/dim]"
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
|
|
346
660
|
def _run_tui(project_root: Path, state: WatchState, runner: ReviewRunner) -> None:
|
|
347
661
|
"""Render the live Graphite-style graph and consume the queue serially."""
|
|
348
662
|
from refactorai_cli.main import _print_agent_intro
|
|
@@ -370,26 +684,12 @@ def _run_tui(project_root: Path, state: WatchState, runner: ReviewRunner) -> Non
|
|
|
370
684
|
state.move_selection(-1)
|
|
371
685
|
elif key in ("down", "j"):
|
|
372
686
|
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
687
|
elif key == "p":
|
|
390
688
|
console.print(
|
|
391
689
|
"[dim]watch: push arrives in Phase 5; use `git push` for now.[/dim]"
|
|
392
690
|
)
|
|
691
|
+
elif key in ("r", "f", "d", "m", "x", "v", "b"):
|
|
692
|
+
_dispatch_key(live, project_root, key, state)
|
|
393
693
|
refresh(state)
|
|
394
694
|
finally:
|
|
395
695
|
live.stop()
|
|
@@ -418,6 +718,10 @@ def watch(
|
|
|
418
718
|
_ensure_auth_in_window(json_output)
|
|
419
719
|
|
|
420
720
|
state = WatchState(repo_name=project_root.name)
|
|
721
|
+
try:
|
|
722
|
+
state.current_branch = git_scope.current_branch(project_root) or ""
|
|
723
|
+
except Exception:
|
|
724
|
+
state.current_branch = ""
|
|
421
725
|
for event in _load_initial_events(project_root, since, limit):
|
|
422
726
|
sha = str(event.get("sha") or "")
|
|
423
727
|
if sha and not state.has(sha):
|