# 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 reverse 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. ## Task header docs/handoff/NEXT_RX_TASK.md starts with these plain key-value fields: TASK_ID = RF001A_OTA_CAPTURE_002 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. 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_002; - 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 clones only the existing result branch into a temporary directory, writes exactly: docs/handoff/results/RX_RESULT.json docs/handoff/results/RX_RESULT.md It checks the staged path set, commits, and pushes HEAD to origin/rx-node. 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. For a new valid READY result it checks task ID, 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 the checks. A changed result, stale publication or fewer than 10 seconds remaining blocks authorization. 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. ## Dry-run contract The fake coordination tests use a temporary local bare Git repository and branch named rx-node. They publish a fake READY result, detect it, require the exact RED phrase and invoke a fake TX action that writes RF TX = 0. They do not fetch the real remote, access SDR hardware, run hackrf_transfer, create the production rx-node branch or transmit RF.