7.5 KiB
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_003
TASK_TITLE = Start coordinated RF-001A common-clock OTA capture
SOURCE_BRANCH = video-development
EXPECTED_HEAD = <full 40-character base commit hash>
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 <TASK_ID>. 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/<TASK_ID>/. 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 <TASK_ID>
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-json> --result-branch rx-node
The publisher checks:
- the current task and result TASK_ID agree;
- ALLOW_PUSH = RX_RESULT_ONLY;
- RF_TX = NO;
- last_approved_task_id matches;
- result source_head equals the current checkout HEAD;
- origin/rx-node already exists;
- 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 and handling. 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.
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.
Model-free channel and dry-run contracts
The preflight channel command publishes synthetic READY and COMPLETE results with TASK_ID RF_HANDOFF_CHANNEL_TEST_001 through the real origin. The MAIN watcher must observe both without a RED prompt, and COMPLETE must be the final remote state. It does not access SDR hardware or run hackrf_transfer.
Automated coordination tests use a temporary local bare Git repository and branch named rx-node. They cover worktree publication, READY, COMPLETE, push failure and watcher reception without accessing the real remote or SDR hardware and without transmitting RF.