Add bidirectional RF handoff coordination

This commit is contained in:
LittleSam129
2026-09-21 17:02:49 +03:00
parent 34f5ace545
commit bc55d0516d
14 changed files with 1978 additions and 556 deletions

View File

@@ -17,6 +17,12 @@ from typing import Callable, Protocol
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from tools.rf_handoff_channel import GitResultChannel, load_result # noqa: E402
DEFAULT_TASK_PATH = "docs/handoff/NEXT_RX_TASK.md"
DEFAULT_STATE_PATH = ROOT / ".local/rx_agent_state.json"
DEFAULT_OUTBOX = ROOT / ".local/rx_agent_outbox"
@@ -71,6 +77,7 @@ class WatcherState:
last_seen_task_id: str | None = None
last_completed_task_id: str | None = None
last_approved_task_id: str | None = None
deferred_task_id: str | None = None
running: bool = False
pid: int | None = None
@@ -404,6 +411,7 @@ def handle_task(
if decision != ApprovalDecision.APPROVE:
return "APPROVAL_BLOCKED", state
state.deferred_task_id = None
state.last_approved_task_id = task.task_id
paths = executor.prepare(task, task_text, source_head, dry_run=dry_run)
output_function(f"TASK PREPARED = {paths[0]}")
return "PREPARED", state
@@ -476,8 +484,58 @@ def mark_completed(state_path: Path, task_id: str) -> None:
save_state(state_path, state)
def publish_result(arguments: argparse.Namespace) -> int:
"""Publish only the approved small RX result to an existing branch."""
repository = arguments.repository.resolve()
task = parse_task((repository / DEFAULT_TASK_PATH).read_text(encoding="utf-8-sig"))
state = load_state(arguments.state_file.resolve())
result = load_result(arguments.publish_result.resolve())
if task.task_id != result.task_id:
raise ValueError("result TASK_ID disagrees with current RX task")
if task.allow_push != "RX_RESULT_ONLY" or task.rf_tx:
raise ValueError("current task does not allow RX result publication")
if state.last_approved_task_id != result.task_id:
raise ValueError("current RX task has not passed the local approval gate")
def git_output(arguments: list[str]) -> str:
return subprocess.run(
["git", *arguments],
cwd=repository,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
check=True,
).stdout.strip()
branch = git_output(["branch", "--show-current"])
if branch != task.source_branch:
raise ValueError("current branch disagrees with task SOURCE_BRANCH")
if git_output(["status", "--porcelain", "--untracked-files=no"]):
raise ValueError("tracked worktree must be clean before result publication")
head = git_output(["rev-parse", "HEAD"])
remote_head = git_output(["rev-parse", f"{arguments.remote}/{task.source_branch}"])
if head != remote_head:
raise ValueError("current HEAD disagrees with the remote task branch")
if result.source_head != head:
raise ValueError("result SOURCE_HEAD disagrees with current checkout")
channel = GitResultChannel(
repository,
remote=arguments.remote,
branch=arguments.result_branch,
)
outcome = channel.publish(result)
print(f"RX RESULT PUBLISH = {outcome.status}")
print(f"RX RESULT HEAD = {outcome.commit}")
print("RF TX = 0")
return 0
def run_watcher(arguments: argparse.Namespace) -> int:
state_path = arguments.state_file.resolve()
if arguments.publish_result is not None:
return publish_result(arguments)
if arguments.request_stop:
stopped = request_stop(state_path, arguments.stop_timeout)
print("WATCHER STOPPED" if stopped else "WATCHER STOP TIMEOUT")
@@ -576,6 +634,8 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--request-stop", action="store_true")
parser.add_argument("--stop-timeout", type=float, default=35.0)
parser.add_argument("--mark-completed")
parser.add_argument("--publish-result", type=Path)
parser.add_argument("--result-branch", default="rx-node")
return parser