From 7c68ccc1daec9733b0680ac80ace6b4a0c0fc72a Mon Sep 17 00:00:00 2001 From: ayazhankadessova Date: Tue, 17 Feb 2026 20:39:48 +0300 Subject: [PATCH 1/4] [docs]: Update FSM documentation for rebase-to-review transition for issue #943. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - python/agentize/workflow/impl/impl.md: Update Mermaid diagram to show rebase→review instead of rebase→impl. - python/agentize/workflow/impl/transition.md: Update transition coverage from rebase→impl/fatal to rebase→review/fatal. - python/agentize/workflow/impl/kernels.md: Document review counter reset behavior in rebase_stage_kernel on EVENT_REBASE_OK. - src/cli/lol/commands/README.md: Add rebase.sh / _lol_cmd_rebase to file map table. --- python/agentize/workflow/impl/impl.md | 2 +- python/agentize/workflow/impl/kernels.md | 3 ++- python/agentize/workflow/impl/transition.md | 2 +- src/cli/lol/commands/README.md | 1 + 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/python/agentize/workflow/impl/impl.md b/python/agentize/workflow/impl/impl.md index 6b6ec8aa..180ab666 100644 --- a/python/agentize/workflow/impl/impl.md +++ b/python/agentize/workflow/impl/impl.md @@ -83,7 +83,7 @@ flowchart LR pr -->|pr_fail_fixable| impl pr -->|pr_fail_need_rebase| rebase[rebase] pr -->|6x failures| fatal - rebase -->|rebase_ok| impl + rebase -->|rebase_ok| review rebase -->|rebase_conflict| fatal ``` diff --git a/python/agentize/workflow/impl/kernels.md b/python/agentize/workflow/impl/kernels.md index e0a2634a..452ed9fe 100644 --- a/python/agentize/workflow/impl/kernels.md +++ b/python/agentize/workflow/impl/kernels.md @@ -325,7 +325,8 @@ kernel return values and the current state. - `pr_stage_kernel(context)`: Calls `pr_kernel()`, passes through event, tracks `pr_attempts` (limit 6), emits `EVENT_PR_PASS`/`EVENT_PR_FAIL_*`/`EVENT_FATAL` - `rebase_stage_kernel(context)`: Calls `rebase_kernel()`, passes through event, - tracks `rebase_attempts` (limit 3), emits `EVENT_REBASE_OK`/`EVENT_REBASE_CONFLICT`/`EVENT_FATAL` + tracks `rebase_attempts` (limit 3), resets `review_attempts`/`review_fail_streak`/`last_review_score` + on `EVENT_REBASE_OK` (so review starts fresh after rebase), emits `EVENT_REBASE_OK`/`EVENT_REBASE_CONFLICT`/`EVENT_FATAL` - `simp_stage_kernel(context)`: Handles `enable_simp=False` → `EVENT_SIMP_PASS`, calls `simp_kernel()`, tracks `simp_attempts` (limit 3), emits `EVENT_SIMP_PASS`/`EVENT_SIMP_FAIL` diff --git a/python/agentize/workflow/impl/transition.md b/python/agentize/workflow/impl/transition.md index bc53ebaa..90c673ca 100644 --- a/python/agentize/workflow/impl/transition.md +++ b/python/agentize/workflow/impl/transition.md @@ -31,7 +31,7 @@ The table includes the expected implementation flow: - `review -> simp/impl/fatal` - `simp -> pr/impl/fatal` - `pr -> finish/impl/rebase/fatal` -- `rebase -> impl/fatal` +- `rebase -> review/fatal` Terminal stages (`finish`, `fatal`) currently only accept `fatal` as an explicit fallback route. diff --git a/src/cli/lol/commands/README.md b/src/cli/lol/commands/README.md index 320d4c53..9b4d0453 100644 --- a/src/cli/lol/commands/README.md +++ b/src/cli/lol/commands/README.md @@ -17,6 +17,7 @@ Per-command implementation files for the `lol` CLI. Each file exports exactly on | `usage.sh` | `_lol_cmd_usage` | Report Claude Code token usage statistics | | `plan.sh` | `_lol_cmd_plan` | Run multi-agent debate pipeline | | `impl.sh` | `_lol_cmd_impl` | Automate issue-to-implementation loop | +| `rebase.sh` | `_lol_cmd_rebase` | Rebase current branch onto target branch | ## Design From 0428f9de60a2b6aaa1da49fb6ad47335a52252b4 Mon Sep 17 00:00:00 2001 From: ayazhankadessova Date: Tue, 17 Feb 2026 20:41:04 +0300 Subject: [PATCH 2/4] [feat]: Fix FSM rebase-to-review transition and add tests for issue #943. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - python/agentize/workflow/impl/transition.py: Change (STAGE_REBASE, EVENT_REBASE_OK) target from STAGE_IMPL to STAGE_REVIEW. - python/agentize/workflow/impl/kernels.py: Reset review_fail_streak, review_attempts, and last_review_score on EVENT_REBASE_OK in rebase_stage_kernel. - python/tests/test_impl_fsm.py: Add rebase→review transition assertion, review counter reset test, and rebase→review→simp→pr→finish orchestrator test. All 39 tests pass. --- python/agentize/workflow/impl/kernels.py | 3 ++ python/agentize/workflow/impl/transition.py | 2 +- python/tests/test_impl_fsm.py | 58 +++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/python/agentize/workflow/impl/kernels.py b/python/agentize/workflow/impl/kernels.py index 0d61d94a..adf0d787 100644 --- a/python/agentize/workflow/impl/kernels.py +++ b/python/agentize/workflow/impl/kernels.py @@ -294,6 +294,9 @@ def rebase_stage_kernel(context: WorkflowContext) -> StageResult: if event == EVENT_REBASE_OK: state.iteration += 1 + context.data["review_fail_streak"] = 0 + context.data["review_attempts"] = 0 + context.data["last_review_score"] = None return StageResult(event=event, reason=message) diff --git a/python/agentize/workflow/impl/transition.py b/python/agentize/workflow/impl/transition.py index 7217281f..908a34a6 100644 --- a/python/agentize/workflow/impl/transition.py +++ b/python/agentize/workflow/impl/transition.py @@ -49,7 +49,7 @@ (STAGE_PR, EVENT_PR_FAIL_FIXABLE): STAGE_IMPL, (STAGE_PR, EVENT_PR_FAIL_NEED_REBASE): STAGE_REBASE, (STAGE_PR, EVENT_FATAL): STAGE_FATAL, - (STAGE_REBASE, EVENT_REBASE_OK): STAGE_IMPL, + (STAGE_REBASE, EVENT_REBASE_OK): STAGE_REVIEW, (STAGE_REBASE, EVENT_REBASE_CONFLICT): STAGE_FATAL, (STAGE_REBASE, EVENT_FATAL): STAGE_FATAL, (STAGE_FINISH, EVENT_FATAL): STAGE_FATAL, diff --git a/python/tests/test_impl_fsm.py b/python/tests/test_impl_fsm.py index c335d84a..71318d12 100644 --- a/python/tests/test_impl_fsm.py +++ b/python/tests/test_impl_fsm.py @@ -89,6 +89,7 @@ def test_next_stage_resolves_expected_edges(self): assert next_stage(STAGE_SIMP, EVENT_SIMP_PASS) == STAGE_PR assert next_stage(STAGE_SIMP, EVENT_SIMP_FAIL) == STAGE_IMPL assert next_stage(STAGE_PR, EVENT_PR_PASS) == STAGE_FINISH + assert next_stage(STAGE_REBASE, EVENT_REBASE_OK) == STAGE_REVIEW def test_next_stage_raises_on_unknown_stage(self): with pytest.raises(TransitionError, match="Unknown stage"): @@ -158,6 +159,44 @@ def pr_kernel(ctx: WorkflowContext) -> StageResult: assert any("stage=simp event=simp_pass" in line for line in logs) assert any("stage=pr event=pr_pass" in line for line in logs) + def test_run_fsm_orchestrator_rebase_ok_reaches_review_then_finish(self): + context = WorkflowContext(plan="p", upstream_instruction="u") + context.current_stage = STAGE_REBASE + logs: list[str] = [] + + def rebase_kernel(ctx: WorkflowContext) -> StageResult: + return StageResult(event=EVENT_REBASE_OK, reason="rebased") + + def review_kernel(ctx: WorkflowContext) -> StageResult: + return StageResult(event=EVENT_REVIEW_PASS, reason="review ok") + + def simp_kernel(ctx: WorkflowContext) -> StageResult: + return StageResult(event=EVENT_SIMP_PASS, reason="simp ok") + + def pr_kernel(ctx: WorkflowContext) -> StageResult: + return StageResult(event=EVENT_PR_PASS, reason="pr ok") + + kernels = { + STAGE_REBASE: rebase_kernel, + STAGE_REVIEW: review_kernel, + STAGE_SIMP: simp_kernel, + STAGE_PR: pr_kernel, + } + + result = run_fsm_orchestrator( + context, + kernels=kernels, + logger=logs.append, + max_steps=10, + ) + + assert result.current_stage == STAGE_FINISH + assert result.final_status == "finish" + assert any("stage=rebase event=rebase_ok" in line for line in logs) + assert any("stage=review event=review_pass" in line for line in logs) + assert any("stage=simp event=simp_pass" in line for line in logs) + assert any("stage=pr event=pr_pass" in line for line in logs) + def test_run_fsm_orchestrator_marks_fatal_on_missing_kernel(self): context = WorkflowContext(plan="p", upstream_instruction="u") result = run_fsm_orchestrator(context, kernels={}, logger=lambda _msg: None) @@ -539,6 +578,25 @@ def test_rebase_ok_passthrough(self, tmp_path: Path, monkeypatch): assert result.event == EVENT_REBASE_OK assert state.iteration == initial_iter + 1 + def test_rebase_ok_resets_review_counters(self, tmp_path: Path, monkeypatch): + context = _make_impl_context( + tmp_path, + review_fail_streak=2, + review_attempts=3, + last_review_score=50, + ) + + monkeypatch.setattr( + "agentize.workflow.impl.kernels.rebase_kernel", + lambda state, **kw: (EVENT_REBASE_OK, "Rebased", tmp_path / "rebase.json"), + ) + + result = rebase_stage_kernel(context) + assert result.event == EVENT_REBASE_OK + assert context.data["review_fail_streak"] == 0 + assert context.data["review_attempts"] == 0 + assert context.data["last_review_score"] is None + def test_rebase_attempts_4_returns_fatal(self, tmp_path: Path): context = _make_impl_context(tmp_path, rebase_attempts=3) From 57784afc8edeb2443aee2ae38ab3734b5a0c47b6 Mon Sep 17 00:00:00 2001 From: ayazhankadessova Date: Tue, 17 Feb 2026 20:46:07 +0300 Subject: [PATCH 3/4] [feat][agent.command]: Add standalone `lol rebase` CLI command for issue #943. - python/agentize/cli.py: Add `handle_rebase()` with auto-detect main/master, fetch, rebase, and conflict abort. Add `rebase` subparser with `--target-branch`. - src/cli/lol/commands/rebase.sh: New `_lol_cmd_rebase` delegating to Python CLI. - src/cli/lol/parsers.sh: Add `_lol_parse_rebase` with `--target-branch` flag. - src/cli/lol/dispatch.sh: Add `rebase)` case and help text entry. - src/cli/lol/commands.sh: Source `commands/rebase.sh`. - src/cli/lol/completion.sh: Add `rebase` command and `rebase-flags` completion. All 39 tests pass. --- python/agentize/cli.py | 58 ++++++++++++++++++++++++++++++++++ src/cli/lol/commands.sh | 1 + src/cli/lol/commands/rebase.sh | 17 ++++++++++ src/cli/lol/completion.sh | 4 +++ src/cli/lol/dispatch.sh | 4 +++ src/cli/lol/parsers.sh | 44 ++++++++++++++++++++++++++ 6 files changed, 128 insertions(+) create mode 100644 src/cli/lol/commands/rebase.sh diff --git a/python/agentize/cli.py b/python/agentize/cli.py index 1eafe990..d60508ca 100644 --- a/python/agentize/cli.py +++ b/python/agentize/cli.py @@ -107,6 +107,52 @@ def handle_simp(args: argparse.Namespace) -> int: return 1 +def handle_rebase(args: argparse.Namespace) -> int: + """Handle rebase command — fetch and rebase onto target branch.""" + import subprocess + + target_branch = getattr(args, "target_branch", None) or "main" + remote = "origin" + + # Detect default branch if not explicitly provided + if not getattr(args, "target_branch", None): + for candidate in ("main", "master"): + result = subprocess.run( + ["git", "rev-parse", "--verify", f"{remote}/{candidate}"], + capture_output=True, + ) + if result.returncode == 0: + target_branch = candidate + break + + # Fetch + print(f"Fetching from {remote}...") + fetch_result = subprocess.run(["git", "fetch", remote], capture_output=True, text=True) + if fetch_result.returncode != 0: + print(f"Error: Failed to fetch from {remote}", file=sys.stderr) + if fetch_result.stderr: + print(fetch_result.stderr, file=sys.stderr) + return 1 + + # Rebase + ref = f"{remote}/{target_branch}" + print(f"Rebasing onto {ref}...") + rebase_result = subprocess.run(["git", "rebase", ref], capture_output=True, text=True) + if rebase_result.returncode != 0: + print(f"Error: Rebase conflict detected against {ref}.", file=sys.stderr) + if rebase_result.stderr: + print(rebase_result.stderr, file=sys.stderr) + # Abort the failed rebase to leave the branch clean + subprocess.run(["git", "rebase", "--abort"], capture_output=True) + return 1 + + if "is up to date" in (rebase_result.stdout or ""): + print(f"Already up to date with {ref}.") + else: + print(f"Successfully rebased onto {ref}.") + return 0 + + def handle_usage(args: argparse.Namespace) -> int: """Handle usage command.""" mode = "week" if args.week else "today" @@ -253,6 +299,16 @@ def main() -> int: help="Issue number to publish the report when approved", ) + # rebase command + rebase_parser = subparsers.add_parser( + "rebase", help="Rebase current branch onto target branch" + ) + rebase_parser.add_argument( + "--target-branch", + dest="target_branch", + help="Target branch to rebase onto (default: auto-detect main/master)", + ) + # version command subparsers.add_parser("version", help="Display version information") @@ -282,6 +338,8 @@ def main() -> int: return handle_impl(args) elif args.command == "simp": return handle_simp(args) + elif args.command == "rebase": + return handle_rebase(args) elif args.command == "version": return handle_version(agentize_home) else: diff --git a/src/cli/lol/commands.sh b/src/cli/lol/commands.sh index aeec2c7f..c0a9d356 100644 --- a/src/cli/lol/commands.sh +++ b/src/cli/lol/commands.sh @@ -24,3 +24,4 @@ source "$_LOL_COMMANDS_DIR/commands/usage.sh" source "$_LOL_COMMANDS_DIR/commands/plan.sh" source "$_LOL_COMMANDS_DIR/commands/impl.sh" source "$_LOL_COMMANDS_DIR/commands/simp.sh" +source "$_LOL_COMMANDS_DIR/commands/rebase.sh" diff --git a/src/cli/lol/commands/rebase.sh b/src/cli/lol/commands/rebase.sh new file mode 100644 index 00000000..920fb64b --- /dev/null +++ b/src/cli/lol/commands/rebase.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# lol rebase command implementation +# Delegates to the Python CLI rebase handler + +# Main _lol_cmd_rebase function +# Arguments: +# $1 - target_branch: Target branch to rebase onto (optional, auto-detects main/master) +_lol_cmd_rebase() { + local target_branch="$1" + + local target_flag="" + if [ -n "$target_branch" ]; then + target_flag="--target-branch $target_branch" + fi + + python -m agentize.cli rebase $target_flag +} diff --git a/src/cli/lol/completion.sh b/src/cli/lol/completion.sh index dd9984fe..b7847191 100644 --- a/src/cli/lol/completion.sh +++ b/src/cli/lol/completion.sh @@ -19,6 +19,7 @@ _lol_complete() { echo "plan" echo "impl" echo "simp" + echo "rebase" ;; upgrade-flags) echo "--keep-branch" @@ -64,6 +65,9 @@ _lol_complete() { echo "--focus" echo "--issue" ;; + rebase-flags) + echo "--target-branch" + ;; *) # Unknown topic, return empty return 0 diff --git a/src/cli/lol/dispatch.sh b/src/cli/lol/dispatch.sh index f1a028e8..f2016db4 100644 --- a/src/cli/lol/dispatch.sh +++ b/src/cli/lol/dispatch.sh @@ -91,6 +91,9 @@ lol() { usage) _lol_parse_usage "$@" ;; + rebase) + _lol_parse_rebase "$@" + ;; version) _lol_log_version _lol_cmd_version @@ -114,6 +117,7 @@ lol() { echo " lol simp [file] --editor" echo " lol impl [--backend ] [--max-iterations ] [--yolo] [--wait-for-ci]" echo " lol usage [--today | --week] [--cache] [--cost]" + echo " lol rebase [--target-branch ]" echo " lol claude-clean [--dry-run]" echo "" echo "Flags:" diff --git a/src/cli/lol/parsers.sh b/src/cli/lol/parsers.sh index ee3e6642..82fe3e6c 100644 --- a/src/cli/lol/parsers.sh +++ b/src/cli/lol/parsers.sh @@ -615,3 +615,47 @@ _lol_parse_simp() { _lol_cmd_simp "$file_path" "$issue_number" "$focus" } + +# Parse rebase command arguments and call _lol_cmd_rebase +_lol_parse_rebase() { + local target_branch="" + + # Handle --help + if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then + echo "lol rebase: Rebase current branch onto target branch" + echo "" + echo "Usage: lol rebase [--target-branch ]" + echo "" + echo "Options:" + echo " --target-branch Target branch to rebase onto (default: auto-detect main/master)" + echo " --help Show this help message" + return 0 + fi + + while [ $# -gt 0 ]; do + case "$1" in + --target-branch) + shift + if [ -z "$1" ]; then + echo "Error: --target-branch requires a branch name" >&2 + echo "Usage: lol rebase [--target-branch ]" >&2 + return 1 + fi + target_branch="$1" + shift + ;; + -*) + echo "Error: Unknown option '$1'" >&2 + echo "Usage: lol rebase [--target-branch ]" >&2 + return 1 + ;; + *) + echo "Error: Unexpected argument '$1'" >&2 + echo "Usage: lol rebase [--target-branch ]" >&2 + return 1 + ;; + esac + done + + _lol_cmd_rebase "$target_branch" +} From 05d4850aee8adf148531e09cef63b59c465d5af4 Mon Sep 17 00:00:00 2001 From: ayazhankadessova Date: Tue, 17 Feb 2026 20:52:40 +0300 Subject: [PATCH 4/4] [bugfix][agent.command]: Fix shell word-splitting and review feedback for issue #943. - src/cli/lol/commands/rebase.sh: Use conditional branching instead of string concatenation to avoid word-splitting/zsh incompatibility. - python/agentize/cli.py: Replace getattr() with direct args.target_branch access for consistency with other handlers. - python/agentize/cli.md: Add `rebase` and `plan` commands to Commands table. All 39 FSM tests pass. --- python/agentize/cli.md | 2 ++ python/agentize/cli.py | 4 ++-- src/cli/lol/commands/rebase.sh | 7 +++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/python/agentize/cli.md b/python/agentize/cli.md index 174d57f2..5205df24 100644 --- a/python/agentize/cli.md +++ b/python/agentize/cli.md @@ -20,8 +20,10 @@ The Python CLI supports the same commands as the shell implementation: | `usage` | Report Claude Code token usage statistics (--cache, --cost) | | `claude-clean` | Remove stale project entries from `~/.claude.json` | | `version` | Display version information | +| `plan` | Run multi-agent debate pipeline (optional `--dry-run`, `--verbose`) | | `impl` | Issue-to-implementation loop (Python workflow, optional `--wait-for-ci`) | | `simp` | Simplify code without changing semantics (optional `--focus`, `--issue`) | +| `rebase` | Rebase current branch onto target branch (optional `--target-branch`) | ## Top-level Flags diff --git a/python/agentize/cli.py b/python/agentize/cli.py index d60508ca..5187e418 100644 --- a/python/agentize/cli.py +++ b/python/agentize/cli.py @@ -111,11 +111,11 @@ def handle_rebase(args: argparse.Namespace) -> int: """Handle rebase command — fetch and rebase onto target branch.""" import subprocess - target_branch = getattr(args, "target_branch", None) or "main" + target_branch = args.target_branch or "main" remote = "origin" # Detect default branch if not explicitly provided - if not getattr(args, "target_branch", None): + if not args.target_branch: for candidate in ("main", "master"): result = subprocess.run( ["git", "rev-parse", "--verify", f"{remote}/{candidate}"], diff --git a/src/cli/lol/commands/rebase.sh b/src/cli/lol/commands/rebase.sh index 920fb64b..8b409ff6 100644 --- a/src/cli/lol/commands/rebase.sh +++ b/src/cli/lol/commands/rebase.sh @@ -8,10 +8,9 @@ _lol_cmd_rebase() { local target_branch="$1" - local target_flag="" if [ -n "$target_branch" ]; then - target_flag="--target-branch $target_branch" + python -m agentize.cli rebase --target-branch "$target_branch" + else + python -m agentize.cli rebase fi - - python -m agentize.cli rebase $target_flag }