Add bidirectional RF handoff coordination
This commit is contained in:
@@ -2,84 +2,139 @@
|
||||
|
||||
## 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 never executed until the user approves it. The first implementation uses only `ManualExecutor`: it prepares a local task outbox and does not start a model itself.
|
||||
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:
|
||||
docs/handoff/NEXT_RX_TASK.md starts with these plain key-value fields:
|
||||
|
||||
```text
|
||||
TASK_ID = RF001A_CLKIN_001
|
||||
TASK_TITLE = Verify RF-001A slave CLKIN
|
||||
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 = RF001A_OTA_CAPTURE_002
|
||||
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` names the known base commit against which the task was written; the watcher requires it to be an ancestor of the fetched task commit. This avoids an impossible self-reference to the commit that contains the header. `ALLOW_PUSH = RX_RESULT_ONLY` permits only a small result publication to `rx-node`; it never permits a push to `video-development`. `ALLOW_COMMIT` governs task changes in the source checkout; a separately authorized result-only commit on `rx-node` follows `ALLOW_PUSH`. `RISK_LEVEL` is an optional plain field used to declare RED actions other than RF TX.
|
||||
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.
|
||||
|
||||
The watcher rejects missing, duplicate or invalid fields. It reads the file with `git show origin/video-development:docs/handoff/NEXT_RX_TASK.md`, so it does not need to change the local checkout.
|
||||
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, gain or bounded RF RX | The user must choose `[A] Approve` |
|
||||
| RED | RF TX, firmware, CPLD, Zadig, USB driver changes, destructive Git or filesystem operations | A normal approval is insufficient; an executor must require the exact separate action `AUTHORIZE RF TX` or an equivalent task-specific explicit confirmation |
|
||||
| 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. Firmware, CPLD, Zadig, USB driver changes and destructive actions must declare `RISK_LEVEL = RED`. A non-RF RED approval requires `AUTHORIZE <TASK_ID>`; RF TX requires exactly `AUTHORIZE RF TX`. The watcher cannot authorize RED itself, and the executor must reject a body/header conflict.
|
||||
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 prompt the minimum choices are:
|
||||
At the RX prompt the choices are:
|
||||
|
||||
```text
|
||||
[A] Approve
|
||||
[D] Defer
|
||||
[S] Stop watcher
|
||||
```
|
||||
[A] Approve
|
||||
[D] Defer
|
||||
[S] Stop watcher
|
||||
|
||||
The watcher records the task ID before leaving the prompt. Deferred and already seen IDs are not shown again on every polling cycle. A changed task must have a new `TASK_ID`.
|
||||
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>/`. `.local/` is ignored by Git. State includes `last_seen_task_id`, `last_completed_task_id`, `deferred_task_id`, `running` and the watcher PID. Writes use a temporary file followed by an atomic replace.
|
||||
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.
|
||||
|
||||
`TaskExecutor` is the backend interface. Version 1 provides `ManualExecutor`, which writes `TASK.md`, `RX_RESULT.md` and `RX_RESULT.json` after approval. It refuses to overwrite an existing task outbox. It does not invoke an agent, access SDR hardware, commit or push. A `CodexExecutor` may be added only after the Codex CLI is found and tested on the RX laptop. Availability on the main PC is not sufficient evidence.
|
||||
State includes:
|
||||
|
||||
After a human or separately authorized executor finishes the task, mark the local ID complete:
|
||||
- last_seen_task_id;
|
||||
- last_completed_task_id;
|
||||
- last_approved_task_id;
|
||||
- deferred_task_id;
|
||||
- running;
|
||||
- watcher PID.
|
||||
|
||||
```powershell
|
||||
python -B tools/rx_agent_watcher.py --mark-completed <TASK_ID>
|
||||
```
|
||||
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.
|
||||
|
||||
## Result and `rx-node` branch
|
||||
After the task actually finishes, mark the local ID complete:
|
||||
|
||||
The result follows [RX_RESULT_TEMPLATE.md](RX_RESULT_TEMPLATE.md) and has matching Markdown and JSON. The proposed publication branch is `rx-node`.
|
||||
python -B tools/rx_agent_watcher.py --mark-completed <TASK_ID>
|
||||
|
||||
The RX laptop must never commit or push to `video-development`. A separately approved publication step may place only these items on `rx-node`:
|
||||
## Strict result schema
|
||||
|
||||
- handoff result Markdown;
|
||||
- result JSON;
|
||||
- small log metadata needed to interpret the result.
|
||||
The coordinated RF-001A result is a strict JSON object. Unknown or missing keys are rejected. READY requires:
|
||||
|
||||
Raw IQ, video, waveform files, large logs, secrets and machine-local state remain outside Git. The watcher does not create `rx-node`, switch branches, commit or push. This keeps branch creation and publication subject to the repository's current instructions and a separate explicit authorization.
|
||||
- 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.
|
||||
|
||||
The main PC later uses `git fetch origin rx-node` and reads the result without merging it blindly.
|
||||
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:
|
||||
|
||||
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
|
||||
|
||||
- Git polling itself does not call an LLM.
|
||||
- When no new task exists, no model runs.
|
||||
- A detected but unapproved or deferred task does not run a model.
|
||||
- After approval, `ManualExecutor` only prepares an outbox. A separately selected executor may invoke one agent.
|
||||
- The executor stops after task completion. The watcher returns to Git polling and consumes no model tokens.
|
||||
- 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
|
||||
|
||||
Dry-run requires a local `--task-file`; it does not fetch Git, access SDR, invoke an agent, perform RF RX/TX, commit or push. The tracked fake task is `tests/fixtures/watcher_test_task.md` with `TASK_ID = WATCHER_TEST_001` and `RF_TX = NO`.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user