# RX agent handoff protocol ## Purpose The RX laptop watches origin/video-development with ordinary local Python code. Git polling does not call an LLM, ChatGPT or an agent. A detected task is not prepared until the user approves it. The RX-to-MAIN result channel uses the separate origin/rx-node branch. The RX laptop publishes small machine-readable readiness and completion results. The MAIN PC reads those results without merging rx-node into video-development. The MAIN-to-RX acknowledgement channel uses the independent origin/tx-node branch. The MAIN watcher publishes evidence that it observed one exact rx-node commit and digest. The RX laptop scans the acknowledgement history without merging tx-node into video-development. ## Task header docs/handoff/NEXT_RX_TASK.md starts with these plain key-value fields: TASK_ID = RF001A_OTA_CAPTURE_003 TASK_TITLE = Start coordinated RF-001A common-clock OTA capture SOURCE_BRANCH = video-development EXPECTED_HEAD = REQUIRES_APPROVAL = YES HARDWARE_ACCESS = YES RF_RX = YES RF_TX = NO ALLOW_COMMIT = NO ALLOW_PUSH = RX_RESULT_ONLY RISK_LEVEL = YELLOW TASK_ID is immutable and unique. A revised task receives a new ID. EXPECTED_HEAD identifies the known base commit against which the task was written; the watcher requires it to be an ancestor of the fetched task commit. ALLOW_PUSH = RX_RESULT_ONLY authorizes publication of the two allowlisted result files to an existing rx-node branch after the task passes its local approval gate. It never permits a push to video-development and never permits branch creation. The watcher rejects missing, duplicate or invalid fields. It reads the task with git show origin/video-development:docs/handoff/NEXT_RX_TASK.md and does not need to change the local checkout. ## Safety levels and approval | Level | Operations | Approval rule | |---|---|---| | GREEN | Software-only checks, parsing and local result preparation | The task's normal approval applies | | YELLOW | Hardware inventory, clock checks, bounded RF RX and result-only publication | The user must choose [A] Approve for the exact TASK_ID | | RED | RF TX, firmware, CPLD, Zadig, USB driver changes, destructive Git or filesystem operations | A normal approval is insufficient; RF TX requires the exact separate phrase AUTHORIZE RF TX | The watcher derives RED whenever RF_TX = YES and rejects a lower declared level. A non-RF RED task requires AUTHORIZE . An RF TX task requires exactly AUTHORIZE RF TX. At the RX prompt the choices are: [A] Approve [D] Defer [S] Stop watcher The watcher records last_approved_task_id only after approval succeeds. Result publication requires the result TASK_ID to match that approved ID. ## Local state and executor boundary Runtime state is stored in .local/rx_agent_state.json. Prepared prompts and result skeletons are stored in .local/rx_agent_outbox//. The .local directory is ignored by Git. State includes: - last_seen_task_id; - last_completed_task_id; - last_approved_task_id; - deferred_task_id; - running; - watcher PID; - confirmed channel-test READY and COMPLETE digests; - matching tx-node acknowledgement commits. ManualExecutor writes TASK.md, RX_RESULT.md and RX_RESULT.json after approval. It does not invoke an agent, access SDR hardware, commit or push. After the task actually finishes, mark the local ID complete: python -B tools/rx_agent_watcher.py --mark-completed ## Strict result schema The coordinated RF-001A result is a strict JSON object. Unknown or missing keys are rejected. READY requires: - task_id = RF001A_OTA_CAPTURE_003; - state = RX_READY_FOR_TX; - rx_ready_for_tx, process_alive, file_growing and common_clock_verified all true; - a positive capture_pid; - 120-second planned duration; - rf_tx = 0; - final capture fields set to null. COMPLETE requires: - state = CAPTURE_COMPLETE; - rx_ready_for_tx, process_alive and file_growing all false; - exact exit code, size, duration and SHA256; - error counters as non-negative integers or null when the tool did not report them; - clkin_after = DETECTED or NOT_DETECTED; - rf_tx_observed = EXPECTED_EXTERNAL_TX or UNKNOWN; - rf_tx = 0. Timestamps use timezone-aware ISO-8601. source_head is the actual local 40-character Git HEAD. ## Publishing to rx-node Publish a prepared local JSON file with: python -B tools/rx_agent_watcher.py --publish-result --result-branch rx-node The publisher checks: 1. the current task and result TASK_ID agree; 2. ALLOW_PUSH = RX_RESULT_ONLY; 3. RF_TX = NO; 4. last_approved_task_id matches; 5. result source_head equals the current checkout HEAD; 6. origin/rx-node already exists; 7. the JSON schema is valid. It checks the branch with git fetch and git ls-remote through the real origin configured in the main checkout. It creates a temporary worktree from that repository and writes exactly: docs/handoff/results/RX_RESULT.json docs/handoff/results/RX_RESULT.md It checks the staged path set, commits, pushes HEAD:rx-node, and confirms that git ls-remote returns the new commit. It then removes the temporary worktree. It never switches the source checkout. It rejects any additional staged path and enforces a 64 KiB limit per result file. Raw IQ, video, waveform files, large logs, secrets and machine-local state remain outside Git. A missing result branch or failed push is reported and never authorizes TX. ## MAIN PC watcher tools/tx_agent_watcher.py polls origin/rx-node every 5 seconds by default. It does not call an LLM or open SDR hardware. Its versioned local state stores `last_processed_rx_node_commit`. Each poll processes every commit in `last_processed_rx_node_commit..origin/rx-node` from oldest to newest and reads the result from that exact commit instead of reading only the branch tip. For RF_HANDOFF_CHANNEL_TEST_001 the watcher records synthetic READY and COMPLETE states without requesting RED approval. For a new valid production READY result it checks task ID, source HEAD, current result branch commit, state, live-process flags, common clock, publication age, planned duration and remaining capture time. It then requests the exact phrase AUTHORIZE RF TX. After the phrase, it fetches rx-node again and repeats every check. A changed result or commit, stale publication or fewer than 10 seconds remaining blocks authorization. Historical READY is still observed in order, but an expired capture window never authorizes TX. The history cursor advances only after successful schema validation, handling and required acknowledgement publication. Duplicate content advances the commit cursor without repeating an action. A malformed intermediate result blocks later commits, and a non-fast-forward branch rewrite is rejected. For each successfully handled channel-test event, the MAIN watcher publishes a strict TX_RESULT to origin/tx-node. The object contains task ID, acknowledgement state, exact source RX commit and digest, source HEAD, actual observation timestamp, watcher PID and RF safety fields. A channel-test acknowledgement always has RF_TX_AUTHORIZED false, RF_TX_STARTED false and RF_TX 0. The tx-node publisher stages exactly `docs/handoff/results/TX_RESULT.json` and `docs/handoff/results/TX_RESULT.md`, checks the staged path allowlist and enforces 64 KiB per file. A push failure prevents the rx-node cursor from advancing. Duplicate acknowledgements are suppressed by their source binding. No local state, code, IQ, video, waveform or large log is published. A successful gate writes one local authorization record under .local/tx_authorizations/. The record has rf_tx_started = false and rf_tx = 0. The separately controlled TX executor must re-fetch and compare the result commit and digest immediately before opening hardware. Duplicate result digests are not shown repeatedly. ## Token and model behavior - RX task polling does not call an LLM. - MAIN result polling does not call an LLM. - No model runs while there is no new task or READY result. - ManualExecutor prepares local files only. - Neither watcher starts RF TX. - The TX executor remains a separate RED-controlled action. ## Bidirectional channel and dry-run contracts The completed preflight channel command published synthetic READY and COMPLETE results with TASK_ID RF_HANDOFF_CHANNEL_TEST_001 through origin/rx-node. The MAIN watcher observed both without a RED prompt. The RX watcher now requires exact matching acknowledgements in origin/tx-node before RF001A_OTA_CAPTURE_003 can pass its software gate. Automated coordination tests use a temporary local bare Git repository and independent rx-node and tx-node branches. They cover exact commit and digest binding, duplicate suppression, two commits between polls, stale acknowledgement rejection, missing tx-node, push failure, non-fast-forward protection and a full fake two-way flow without accessing the real remote or SDR hardware and without transmitting RF.