From 4b5dd71cb259b33540ab2d8a888104b780c1e7b7 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Sat, 15 Aug 2026 01:36:55 -0500 Subject: [PATCH 1/4] =?UTF-8?q?docs(plans):=20BEAST-01=20platform=20rebuil?= =?UTF-8?q?d=20=E2=80=94=20make=20the=20repo=20the=20authority?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thirty-five days of deploy tooling has been treating symptoms. This is the design pass the 2026-07-11 Pi->Orin port implied and nobody did. Diagnosis, and it is not code volume: the repo is not the authority on the robot, and no mechanism reports when it is wrong. * Nothing declares what runs. beast-install-systemd-units copies all 17 units and never enables them; which 9 start at boot exists only as symlinks on one NVMe. * Nothing declares what the machine is. 17 facts live only on that disk, including the one-line udev rule the OAK-D cannot open without. * Mechanisms report success when they fail. Seven observed firing this week, tiered honestly against two read-in-code and one from a prior session. Traced the history through git rather than assuming it: Waveshare 037dfca shipped zero deploy/ and zero systemd files. Day one of our ownership (2026-07-11) was a hardware port, and 3777a09 "Add disabled Jetson base service" is the seed of the whole layer. The port silently transferred the environment, the supervision, and the deploy to us; each was improvised on the day it first drew blood. Phases order recording before change: capture, declare enablement, declare the machine, workspace from a manifest, supervision on nodes, ros2_control last (it is the only phase writing new code against firmware we do not own). Anti-goals are load-bearing here. No big-bang rewrite; the robot drove autonomously on 2026-08-15. No vanity deletion -- the ~28,000 SLOC of never-launched vendor packages are inert and removing them fixes nothing. No config-management framework for one robot. No script #17. Co-Authored-By: Claude Opus 5 --- .../2026-08-15-beast-platform-rebuild.md | 249 ++++++++++++++++++ docs/plans/README.md | 1 + 2 files changed, 250 insertions(+) create mode 100644 docs/plans/2026-08-15-beast-platform-rebuild.md diff --git a/docs/plans/2026-08-15-beast-platform-rebuild.md b/docs/plans/2026-08-15-beast-platform-rebuild.md new file mode 100644 index 00000000..88989cad --- /dev/null +++ b/docs/plans/2026-08-15-beast-platform-rebuild.md @@ -0,0 +1,249 @@ +# BEAST-01 platform rebuild — make the repo the authority + +**Status:** PROPOSED — 2026-08-15. Owner-requested after a session that spent its whole +budget patching symptoms. This plan is the design pass that the 2026-07-11 Pi→Orin port +implied and nobody has done. It reframes, but does not delete, the 2026-08-07 strip-down +and the 2026-08-14 vendored-surface plans: those correctly catalogue drift; this one names +why drift keeps being generated. + +## The one-sentence diagnosis + +**The repository is not the authority on the robot, and no mechanism tells you when it is +wrong.** Everything painful is an instance of that. It is not a code-volume problem — every +line-count we measured this session (2,467 ours; ~8,300 glue; ~28,000 inert vendor) failed to +explain the pain, because the pain is structural, not dimensional. + +Three properties, each independently sufficient to cause a bad week: + +1. **Nothing in the repo declares what runs.** `beast-install-systemd-units` copies all 17 + unit files and never runs `systemctl enable`. Which 9 of 17 start at boot exists **only** + as symlinks in `*.target.wants/` on one NVMe, created by hand across 35 days, recorded + nowhere. Restore every file from git onto a fresh disk and the robot boots running nothing, + with no document saying what should be on. +2. **Nothing in the repo declares what the machine is.** 17 facts live only on that disk — + including a one-line udev rule (`03e7`, `MODE 0666`) without which the OAK-D will not open, + five group memberships, ~28 hand-installed `ros-humble-*` packages, four third-party apt + sources, and `pip install --user smbus2`, on which the entire power path depends. +3. **Mechanisms report success when they fail.** See the evidence tier below. This is the one + that makes debugging cost 10× — you never get to investigate one layer, because no layer's + green light is admissible. + +## Evidence tier — what is observed vs. what is read + +Stated explicitly because an earlier draft of this argument blurred the two and inflated the +count by one. (`beast-ctl`'s `mask` verb failed **loudly** — a fictional capability, not a +silent failure. It does not belong on this list.) + +**Observed firing, 2026-08-11 → 2026-08-15 (7):** + +| What | Evidence | +| --- | --- | +| `refs/deploy/beast-01` sat 4 commits stale; the pin workflow ran **green** | GitHub API; ref at `69e411d` while `main` was at `d97a3c8` | +| `beast-slam-save` printed "not running, nothing to save" and **exited 0**, twice, with pid 5392 up | two consecutive contradictory invocations; `ins-beast-slam-save-discovery-race` | +| slam_toolbox alive, `/map` publishing at 0.2 Hz, map frozen at 237×54 | `use_sim_time:=true` leak from `online_async_launch.py` | +| `map→odom` bit-identical for 3 minutes while wheel odometry accumulated ~1.0 m | TF samples; destroyed the map, wedged the costmap at cost 99 under the robot | +| Robot still turning **0.335 rad/s ten seconds** after the command ended | `odom→base_link` yaw samples; `ins-beast-latching-runaway-measured` | +| Three scripts in `/usr/local/sbin` older than the repo, fixes merged, deploy reported success | md5 diff vs `deploy/bin`; `ins-beast-runs-stale-binaries` | +| 17 units byte-identical to git; enablement exists only as symlinks | `systemctl` vs repo comparison | + +**Read in code, not observed firing (2):** `ros2 launch` returns 0 on child node death +(`return_code` set only on exception paths; ros2/launch#666 open, PR #712 unmerged); the verify's +gate restore re-arms on any `gate_before` that is not the literal string `false`, including the +empty string returned when the 8-second read times out. + +**Recorded by a prior session (1):** `deploy-to-beast.sh` built new code, aborted before +restarting, and left `git rev-parse HEAD` reading correct. + +## How it got here — the port, not a drift + +Traced through git, not assumed: + +| Date | Event | +| --- | --- | +| 2025-11-25 → 2026-07-10 | Waveshare `ugv_ws` (`037dfca`, DUDULRX). **Zero `deploy/`, zero systemd files** — verified against the tree | +| **2026-07-11** | First Coldaine commits. Titles: *Adapt Beast bringup for **Jetson** safety*, *Support **JetPack** OpenCV*, *Fix **Humble** rf2o metadata* — and `3777a09` **"Add disabled Jetson base service"** | +| 2026-07-12 | Storage foundation (#2–#5) | +| 2026-08-03 | `1e8a167` subtree-merges the fork into RobotOverview | +| 2026-08-07 | `7e86feb` *"strip automatic safety apparatus + cmd_vel watchdog"* | +| 2026-08-10 | Cockpit outage → **8 deploy files in one day** | +| 2026-08-14 | Autonomy on-ramp → **13 deploy files in one day** | + +122 commits total (70 pre-import, 52 post). Of the 52 touching `robot/`, **33 touch `deploy/`** — +a directory the vendor never shipped and which contains none of the robot's behaviour. + +Day one was a **hardware port**: Raspberry Pi → Jetson Orin Nano Super 8GB (JetPack 6.2.2 / +R36.5). Waveshare's SD image and their `/home/ws/ugv_ws` container path both stopped applying, +which silently transferred three responsibilities to us: + +- **the environment** — the image used to carry it → now 17 undeclared facts +- **the supervision** — a human at a terminal used to be it → now 1,870 lines of bash +- **the deploy** — the image *was* the deploy → now `beast-pull`, 647 lines + +The port was correct; you want the compute. What never happened is the sentence that should +have followed it: *"and therefore we now need a provisioning story, a supervision story, and a +deploy story."* Each got improvised instead, on the day it first drew blood. Read the script +headers in birth order and the pattern is explicit — *"Installed 2026-08-10 after the cockpit +outage"*, *"after Wi-Fi dark period"*, *"added AFTER a posegraph explosion destroyed the only +good map"*. Nobody wrote a deploy system. Thirteen times, something failed quietly and someone +wrote a script so that one thing would not fail quietly again. + +Each script is one of the vanished operator's jobs: `beast-verify` = *is it up?*; +`beast-mission` = *stop*; `beast-deploy-guard` = *don't start, it's mid-build*; `beast-pull` = +*get the new code*; `beast-link-watch` = *wifi died*; `beast-slam-save` = *save the map first*; +`beast-ctl` = *you may do that*. **1,870 lines of bash is the price of the missing human.** + +## Done condition + +All five, and none of them is a line count: + +1. A fresh Orin, flashed and given this repo, reaches a verified-running BEAST-01 with **no + undocumented step** — no tribal knowledge, no "and then you also have to…". +2. `git` states which units are enabled; changing that set is a reviewed diff, not an SSH session. +3. `provision.sh --check` fails loudly when the machine diverges from the repo, and runs in + `beast-verify`. +4. A ROS node dying makes its systemd unit fail. `systemctl is-active` is admissible evidence. +5. The workspace is built from a pinned manifest of upstream packages plus a named, justified + list of what is genuinely ours. + +## Phases + +Ordered so that **everything that only records** precedes **everything that changes**. The +recording phases are zero-risk, and they are the safety net for the rest. They are also urgent +in a way the others are not: that knowledge is one dead NVMe away from being gone, and it decays +out of context fast. + +### P0 — Capture, change nothing *(zero risk; do first)* + +Inputs: live robot over `ssh beast-01-ts`. + +- Dump the enablement set: every unit, `is-enabled` + `is-active`, to a file in the repo. +- Dump the machine facts already inventoried (17 items) into a draft `provision.sh` that is + **not yet run** — the file is a record first, an installer second. +- Emit: `robot/beast/provision/state-2026-08-15.md` — the observed truth, dated. + +Done when: a reader can reconstruct which services run and what the machine has, from git alone. + +### P1 — The repo declares enablement + +- `beast-install-systemd-units` gains an enable/disable pass driven by a list **in the repo**. +- The list is explicit about the never-enable contract (`beast-nav` ships with no `[Install]` + section and stays hand-started; that is a decision, and it should read as one). +- Test in `tools/ci`: the declared set matches the unit files present. + +Done when: `systemctl enable` is never again typed by a human, and "born disabled" cannot recur. + +### P2 — The repo declares the machine + +- `provision.sh`, idempotent, plus `--check`. No Ansible, no Nix, no new dependency — the bug is + *undeclared*, not *not-declared-in-a-framework*, and one robot does not justify a config + management system. +- Wire `--check` into `beast-verify` so divergence is a verify failure, not a discovery. + +Done when: the udev rule, the groups, the apt set, `smbus2`, and linger are all in git and checked. + +### P3 — Workspace from a manifest, not a fork + +The big deletion, and the one the owner has been asking for since *"what packages from ROS 2 are +we shipping?"* + +- Replace the 51,361-line vendored tree with a `.repos` manifest naming upstream packages at + pinned versions. Our entire net delta from the fork point is ~913 lines, most of it the + `/cmd_vel` → `/cmd_vel_nav` retarget, which becomes a remap. +- `ugv_nav` (1,550 SLOC of launch files copied verbatim from `nav2_bringup`, Intel copyright + headers intact, 92-line delta) becomes `nav2_bringup` + our params. +- **Carry forward exactly four things**, each with a written reason: + 1. `beast_power` — genuinely ours; INA219, load-compensated SoC, properly tested + 2. the `twist_mux` arbitration ladder + 3. the nav2 params (`rpp.yaml`, RotationShim, velocity smoother caps) — *tuned knowledge* + 4. the ESP32 T-code protocol table + +Done when: the stack comes up from upstream packages plus those four, and the drive path is +unchanged. + +### P4 — Supervision on nodes, not launch files + +`ExecStart=/bin/bash -lc 'source … && exec ros2 launch …'` puts systemd three levels above +anything real, watching a wrapper that returns 0 when its children die. Replace with direct node +invocation per service, one at a time, starting with `beast-slam` (the change is already drafted +on `claude/beast-slam-direct-node`). + +Done when: killing a node fails its unit. Several of the seven observed silent failures die here. + +### P5 — `ros2_control` for the drive path + +The right destination — `diff_drive_controller` supplies the `cmd_vel` timeout that +`beast-mission` currently fakes in 155 lines of trap handler, plus real joint states and odometry. +**Sequenced last on purpose:** it is the only phase that writes genuinely new code +(a `hardware_interface` plugin speaking Waveshare's JSON protocol — parse inbound `T:1001` at +20 Hz, emit `T:13` on write) against firmware we do not own. Its failure mode is "the robot does +not move," or worse, "moves wrong." + +Do not start P5 until P0–P4 are done and the robot is verifiably healthy on a known-good base. + +## Watch out for + +Traps that will bite a rebuild specifically, each verified this session: + +1. **`cartographer` is not Google Cartographer.** It is a 31-line Waveshare package that + name-shadows it, built only because `ugv_nav/localization_launch.py` calls + `get_package_share_directory('cartographer')` **unconditionally**. Park or delete it and AMCL + breaks. Fix the call before touching the package. +2. **`beast_base` is missing from both build allowlists** (`build_common.sh`, `build_first.sh`). + Already flagged as a live hazard in the vendored-surface plan. A clean rebuild from those + scripts yields a workspace where `beast-ros-base.service` fails and the startup stop never + fires — on a robot whose ESP32 latches velocity. Those scripts also hardcode + `WS=/home/ws/ugv_ws`, a container path the robot has never used. +3. **Four dependencies work only by accident.** `ugv_bringup` launches `robot_localization`, + `ugv_tools` launches `joy`, `ugv_slam` launches `slam_toolbox`/`cartographer_ros`/`rtabmap` — + none declared in `package.xml`. A manifest-based rebuild is exactly what exposes these. +4. **Tuned params look like config but are knowledge.** `rpp.yaml`'s RotationShim wrapping + RegulatedPurePursuit, `allow_reversing: false`, and the effective speed cap living in the + velocity smoother (`[0.26, 0.0, 1.0]`) rather than RPP's `desired_linear_vel` — that shape is + deliberate for a robot with a 104° blind wedge astern. Regenerating defaults silently loses it. +5. **`use_sim_time` defaults to `true`** in `online_async_launch.py` and is applied **after** the + params file, overriding it. Any path that keeps a launch file re-imports this bug. +6. **The install path cannot install.** `deploy-to-beast.sh`'s `install_bin_if_needed` falls + through to a break-glass password no unattended path can supply, prints + `WARN cannot passwordless-install`, and **continues successfully**. Three scripts are stale on + the robot right now because of it. Anything new placed in `/usr/local/sbin` inherits this. +7. **`beast-ctl` must stay root-owned in `/usr/local/sbin`.** `/home/beast` is writable by + `beast`, so allowlisting a path under it in sudoers is a trivial root escalation. It must never + be executed from the repo checkout. +8. **Never command motion from a foreground SSH process.** No `cmd_vel` watchdog exists + (owner decision D8, 2026-08-07). Any test procedure must send explicit zeros; ceasing to publish + is not stopping. Measured: 0.335 rad/s ten seconds after the command ended. +9. **In-place rotation corrupts the map.** Do not use "slow spin to build coverage" as a mapping + prerequisite — it froze `map→odom` for three minutes and wedged the costmap. Prefer translating + survey motion, and verify both that the map grew *and* that `map→odom` is moving. +10. **The verify's gate restore is fail-open** and runs on an hourly timer. +11. **`beast-mission`'s EXIT trap fires at body exit**, so a mission body that post-processes + after driving keeps driving through it. Zero immediately after the drive segment, not at exit. + +## Anti-goals + +- **No big-bang rewrite.** Nav2 autonomously drove 1.9 m and stopped 0.096 m from goal on + 2026-08-15. Every phase must leave a robot that drives. +- **No vanity deletion.** The ~28,000 SLOC of never-launched vendor packages (`openslam_gmapping`, + `teb_local_planner`, `costmap_converter`, `ugv_voice`, …) are **inert**. `beast-pull` rebuilds + only the four always-packages plus what changed, so they are not even a recurring build cost. + Removing them is cosmetic and fixes nothing. They may fall out of P3 for free; they are not a + goal. +- **No config-management framework** for one robot. +- **Do not delete `beast-pull`.** 647 lines, but it works and it is tested. Revisit only after P4. +- **No script #17.** If the answer to a phase is "write another supervisor," the phase is wrong. + +## Verification + +- P0: a second reader reconstructs the running set from git alone, then diffs against the live + robot and finds nothing. +- P1/P2: `tools/ci` tests; `provision.sh --check` green on the live robot; deliberately break one + fact and confirm it fails. +- P3: full stack up from the manifest; `beast-verify` 15/15. +- P4: `kill -9` a node, confirm its unit enters `failed`. This is the phase's whole point. +- P5: bench-first on blocks, then the T2/T3 goal ladder, with explicit-zero stops throughout. + +## Related + +Supersedes nothing outright. Reframes the 2026-08-07 strip-down and the 2026-08-14 +vendored-surface plan: both catalogue drift correctly, and P3 removes the surface that generates +it. The autonomy on-ramp's Phase 0 (`beast-slam.service` repair) is folded into P4. diff --git a/docs/plans/README.md b/docs/plans/README.md index fd830065..c1dc49c6 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -12,6 +12,7 @@ disagree, the code is right and the plan is stale. | Plan | What it covers | Blocking? | | --- | --- | --- | +| [BEAST-01 platform rebuild](2026-08-15-beast-platform-rebuild.md) | **Governing design pass.** The repo is not the authority on the robot and nothing reports when it is wrong: enablement and 17 machine facts exist only on one NVMe, and 7 mechanisms were observed reporting success while failing. Phases: capture → declare enablement → declare the machine → workspace from a manifest → supervision on nodes → `ros2_control`. Reframes the strip-down and vendored-surface plans; folds in the on-ramp's Phase 0. | Not blocking, but P0–P2 are one dead disk away from being unrecoverable | | [BEAST-01 Command Deck + sensor fusion](2026-07-31-beast-command-deck-plan.md) | Historical cockpit context. Its robot-side safety-spine work is superseded by the 2026-08-07 strip-down; the Hangar cockpit remains product scope. | Superseded for robot-side work | | [BEAST ROS 2 strip-down](2026-08-07-beast-ros-drift-inventory-and-stripdown.md) | Remaining BEAST ROS 2 custom-drift strip after #174: Phase 1 extracted `beast_base` from the vendor `ugv_bringup.py` and removed `/ugv/watchdog_state` consumers (**done, #176**); Phase 2 deletes vizanti + `ugv_web_app` and reverts the 12 demo retargets; Phase 3 drift audit + robot ground truth, then the plan is deleted. | Phase 2 partial (H2 neutralized, not deleted); Phase 1 extraction done (#176) | | [Finish the wiring model](2026-07-30-wiring-model-completion.md) | One spine, two eyes: The Board consumes `wiring.ts`, corpus extraction (schematics, firmware, photos, CAD), facts landed with zone citations, operator answers on screen. Merges the 2026-07-27 unification, extraction, and CAD plans. | Q1/Q2 safety-relevant (wrong 40-pin numbering puts 5 V into a Jetson UART pin); X1 gates drilling | From 99edd47dbe07f3c48adb4fa71c9246e6e81a281f Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Sat, 15 Aug 2026 01:44:23 -0500 Subject: [PATCH 2/4] docs(plans): drop the manufactured urgency from P0-P2 The phase ordering was justified with "one dead NVMe away from being gone", which reads as a prediction that the disk is about to fail. There is no evidence of that -- no SMART warning, nothing. Single point of failure is a structural observation, not a forecast. The real justification needs no drama: the recording phases are cheap and cannot break anything, so there is no reason to sequence them later. Co-Authored-By: Claude Opus 5 --- docs/plans/2026-08-15-beast-platform-rebuild.md | 7 ++++--- docs/plans/README.md | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/plans/2026-08-15-beast-platform-rebuild.md b/docs/plans/2026-08-15-beast-platform-rebuild.md index 88989cad..d6f42517 100644 --- a/docs/plans/2026-08-15-beast-platform-rebuild.md +++ b/docs/plans/2026-08-15-beast-platform-rebuild.md @@ -108,9 +108,10 @@ All five, and none of them is a line count: ## Phases Ordered so that **everything that only records** precedes **everything that changes**. The -recording phases are zero-risk, and they are the safety net for the rest. They are also urgent -in a way the others are not: that knowledge is one dead NVMe away from being gone, and it decays -out of context fast. +recording phases are cheap and cannot break anything, and they are the safety net for the rest — +that is the whole justification for putting them first. There is no deadline on them. The mild +structural point is that the state they capture currently has one copy, on a machine with no +backup; that is a single point of failure, not a prediction that anything is about to fail. ### P0 — Capture, change nothing *(zero risk; do first)* diff --git a/docs/plans/README.md b/docs/plans/README.md index c1dc49c6..4430251c 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -12,7 +12,7 @@ disagree, the code is right and the plan is stale. | Plan | What it covers | Blocking? | | --- | --- | --- | -| [BEAST-01 platform rebuild](2026-08-15-beast-platform-rebuild.md) | **Governing design pass.** The repo is not the authority on the robot and nothing reports when it is wrong: enablement and 17 machine facts exist only on one NVMe, and 7 mechanisms were observed reporting success while failing. Phases: capture → declare enablement → declare the machine → workspace from a manifest → supervision on nodes → `ros2_control`. Reframes the strip-down and vendored-surface plans; folds in the on-ramp's Phase 0. | Not blocking, but P0–P2 are one dead disk away from being unrecoverable | +| [BEAST-01 platform rebuild](2026-08-15-beast-platform-rebuild.md) | **Governing design pass.** The repo is not the authority on the robot and nothing reports when it is wrong: enablement and 17 machine facts exist only on one NVMe, and 7 mechanisms were observed reporting success while failing. Phases: capture → declare enablement → declare the machine → workspace from a manifest → supervision on nodes → `ros2_control`. Reframes the strip-down and vendored-surface plans; folds in the on-ramp's Phase 0. | Not blocking. P0–P2 record only and are safe to run any time | | [BEAST-01 Command Deck + sensor fusion](2026-07-31-beast-command-deck-plan.md) | Historical cockpit context. Its robot-side safety-spine work is superseded by the 2026-08-07 strip-down; the Hangar cockpit remains product scope. | Superseded for robot-side work | | [BEAST ROS 2 strip-down](2026-08-07-beast-ros-drift-inventory-and-stripdown.md) | Remaining BEAST ROS 2 custom-drift strip after #174: Phase 1 extracted `beast_base` from the vendor `ugv_bringup.py` and removed `/ugv/watchdog_state` consumers (**done, #176**); Phase 2 deletes vizanti + `ugv_web_app` and reverts the 12 demo retargets; Phase 3 drift audit + robot ground truth, then the plan is deleted. | Phase 2 partial (H2 neutralized, not deleted); Phase 1 extraction done (#176) | | [Finish the wiring model](2026-07-30-wiring-model-completion.md) | One spine, two eyes: The Board consumes `wiring.ts`, corpus extraction (schematics, firmware, photos, CAD), facts landed with zone citations, operator answers on screen. Merges the 2026-07-27 unification, extraction, and CAD plans. | Q1/Q2 safety-relevant (wrong 40-pin numbering puts 5 V into a Jetson UART pin); X1 gates drilling | From 1de6d4cc1099a17cf53fb39439217aad1b45c9f8 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Sat, 15 Aug 2026 01:46:10 -0500 Subject: [PATCH 3/4] =?UTF-8?q?docs(plans):=20add=20the=20kill=20list=20?= =?UTF-8?q?=E2=80=94=20name=20what=20dies,=20per=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The diagnosis said "1,870 lines of bash is the price of the missing human" and then the plan retired none of it. A diagnosis wearing a plan's clothes. Adds a per-script table: what it is, why it exists, and which phase kills it. beast-mission dies at P5 (diff_drive_controller supplies the timeout it fakes). beast-gamepad and install-operator-shortcuts die outright (joy and teleop_twist_joy are upstream). The Wi-Fi trio leaves the repo entirely -- it is UniFi forensics and lives here only because the symptom appeared here. beast-install-systemd-units and install-beast-sudoers are absorbed into provision.sh. beast-verify, beast-ctl and beast-pull shrink hard. 1,870 lines -> ~300-400. 17 units -> ~8. Survivor sizes are marked as estimates, because they are. Also fixes two things the first draft got wrong: * "Do not delete beast-pull" contradicted the whole thesis. It is now re-evaluated at P5, not preserved. * The anti-goals read as brakes on culling. They now explicitly constrain HOW the cull happens, not how much, and the inert-vendor-package point is restated so it cannot be quoted as a reason to cull less. Done condition gains a sixth item -- the only one that is a number rather than a property. Co-Authored-By: Claude Opus 5 --- .../2026-08-15-beast-platform-rebuild.md | 62 ++++++++++++++++--- docs/plans/README.md | 2 +- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/docs/plans/2026-08-15-beast-platform-rebuild.md b/docs/plans/2026-08-15-beast-platform-rebuild.md index d6f42517..25cf5184 100644 --- a/docs/plans/2026-08-15-beast-platform-rebuild.md +++ b/docs/plans/2026-08-15-beast-platform-rebuild.md @@ -104,6 +104,10 @@ All five, and none of them is a line count: 4. A ROS node dying makes its systemd unit fail. `systemctl is-active` is admissible evidence. 5. The workspace is built from a pinned manifest of upstream packages plus a named, justified list of what is genuinely ours. +6. **The kill list below is executed.** `deploy/bin` is roughly 300–400 lines, not 1,870; the + unit count is roughly 8, not 17; and every surviving script has a written reason a standard + mechanism could not do its job. This is the measurable half of the done condition — the other + five are properties, this one is a number. ## Phases @@ -181,6 +185,43 @@ not move," or worse, "moves wrong." Do not start P5 until P0–P4 are done and the robot is verifiably healthy on a known-good base. +## The kill list + +The point of the phases is not to add a provisioning layer on top of what exists — it is to +**retire the layer that exists**. Every script below was written because a capability was +missing; the phases restore the capability, and the script goes. Stated per file so this plan +can be held to it. + +Lines are exact (`wc -l`, 2026-08-15). Survivor sizes marked *(est.)* are estimates, not +measurements — they should be treated as targets to argue with, not as facts. + +| Script | Lines | Exists because | Fate | +| --- | ---: | --- | --- | +| `beast-mission` | 155 | no `cmd_vel` watchdog (D8) | **Deleted at P5.** `diff_drive_controller` supplies the timeout as a contract | +| `beast-deploy-guard` | 36 | `--symlink-install` deploy is not atomic | **Deleted** when deploy becomes atomic | +| `beast-gamepad` | 45 | no teleop story after the port | **Deleted.** `joy` + `teleop_twist_joy` are upstream and do exactly this | +| `install-operator-shortcuts.sh` | 47 | same | **Deleted** with it | +| `beast-wifi-telemetry` | 204 | Wi-Fi drops, no evidence | **Leaves the repo.** This is UniFi forensics; it lives here only because the symptom appeared here | +| `beast-wifi-watch` | 48 | need deauth/CSA frames between samples | Leaves with it | +| `beast-link-watch` | 49 | link dies unattended | Leaves with it | +| `beast-install-systemd-units` | 59 | nothing provisions the machine | **Absorbed into `provision.sh` at P2** | +| `install-beast-sudoers.sh` | 40 | `beast-ctl` needs a root install | Absorbed at P2 | +| `beast-slam-save` | 69 | map lifecycle unmanaged; races on `ros2 node list` | Becomes a lifecycle hook on a real node, not a shell script guessing whether SLAM is up | +| `beast-verify` | 309 | systemd cannot see node health | **P4 retires most of it.** `systemctl is-active` becomes admissible; what remains is a small hardware probe (drive path, pack, i2c) — ~60 *(est.)* | +| `beast-ctl` | 162 | sudoers cannot express a policy | Shrinks once units are root-run and deploy is not user-driven — ~50 *(est.)* | +| `beast-pull` | 647 | there is no deploy system | **Re-evaluated at P5, not preserved.** An atomic deploy collapses it to fetch + swap + verify — ~150 *(est.)*. It survives P0–P4 because it works and nothing better exists yet | +| **Total** | **1,870** | | **~300–400 remaining** *(est.)* | + +Alongside it: **17 systemd units → roughly 8**, once the Wi-Fi trio (3 units + 2 timers) leaves +and the deploy pair collapses. And the vendored workspace, **51,361 lines → a ~30-line `.repos` +manifest** plus the four carried items named in P3. + +Two honesty notes on this table. First, the ~1,500 lines it retires are **load-bearing** lines — +every one is currently doing a job — which is exactly why this is worth doing and why the +inert-vendor-package deletion was not. Second, no phase may delete its script before the +replacement capability is demonstrated on the robot; a fate in this table is a commitment about +sequence, not permission to remove something early. + ## Watch out for Traps that will bite a rebuild specifically, each verified this session: @@ -222,16 +263,21 @@ Traps that will bite a rebuild specifically, each verified this session: ## Anti-goals +These constrain *how* the cull happens. None of them is a reason to cull less — the kill list +above is the deliverable, and a phase that ends with the same number of scripts has failed. + - **No big-bang rewrite.** Nav2 autonomously drove 1.9 m and stopped 0.096 m from goal on - 2026-08-15. Every phase must leave a robot that drives. -- **No vanity deletion.** The ~28,000 SLOC of never-launched vendor packages (`openslam_gmapping`, - `teb_local_planner`, `costmap_converter`, `ugv_voice`, …) are **inert**. `beast-pull` rebuilds - only the four always-packages plus what changed, so they are not even a recurring build cost. - Removing them is cosmetic and fixes nothing. They may fall out of P3 for free; they are not a - goal. -- **No config-management framework** for one robot. -- **Do not delete `beast-pull`.** 647 lines, but it works and it is tested. Revisit only after P4. + 2026-08-15. Every phase must leave a robot that drives, and nothing is deleted before its + replacement is demonstrated on hardware. This bounds sequence, not scope. +- **Do not count inert code as progress.** The ~28,000 SLOC of never-launched vendor packages + (`openslam_gmapping`, `teb_local_planner`, `costmap_converter`, `ugv_voice`, …) cost nothing — + `beast-pull` rebuilds only the four always-packages plus what changed, so they are not even a + recurring build cost. They fall out of P3 for free. Deleting them is not an achievement and must + never be reported as one; the ~1,500 load-bearing lines in the kill list are the real measure. +- **No config-management framework** for one robot. The bug is *undeclared*, not + *not-declared-in-Ansible*. - **No script #17.** If the answer to a phase is "write another supervisor," the phase is wrong. + Every phase must end with fewer moving parts than it started with. ## Verification diff --git a/docs/plans/README.md b/docs/plans/README.md index 4430251c..8a523324 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -12,7 +12,7 @@ disagree, the code is right and the plan is stale. | Plan | What it covers | Blocking? | | --- | --- | --- | -| [BEAST-01 platform rebuild](2026-08-15-beast-platform-rebuild.md) | **Governing design pass.** The repo is not the authority on the robot and nothing reports when it is wrong: enablement and 17 machine facts exist only on one NVMe, and 7 mechanisms were observed reporting success while failing. Phases: capture → declare enablement → declare the machine → workspace from a manifest → supervision on nodes → `ros2_control`. Reframes the strip-down and vendored-surface plans; folds in the on-ramp's Phase 0. | Not blocking. P0–P2 record only and are safe to run any time | +| [BEAST-01 platform rebuild](2026-08-15-beast-platform-rebuild.md) | **Governing design pass.** The repo is not the authority on the robot and nothing reports when it is wrong: enablement and 17 machine facts exist only on one NVMe, and 7 mechanisms were observed reporting success while failing. Phases: capture → declare enablement → declare the machine → workspace from a manifest → supervision on nodes → `ros2_control`. Reframes the strip-down and vendored-surface plans; folds in the on-ramp's Phase 0. | Not blocking. Carries the kill list: `deploy/bin` 1,870 lines → ~300–400, 17 units → ~8, the 51,361-line vendored tree → a manifest | | [BEAST-01 Command Deck + sensor fusion](2026-07-31-beast-command-deck-plan.md) | Historical cockpit context. Its robot-side safety-spine work is superseded by the 2026-08-07 strip-down; the Hangar cockpit remains product scope. | Superseded for robot-side work | | [BEAST ROS 2 strip-down](2026-08-07-beast-ros-drift-inventory-and-stripdown.md) | Remaining BEAST ROS 2 custom-drift strip after #174: Phase 1 extracted `beast_base` from the vendor `ugv_bringup.py` and removed `/ugv/watchdog_state` consumers (**done, #176**); Phase 2 deletes vizanti + `ugv_web_app` and reverts the 12 demo retargets; Phase 3 drift audit + robot ground truth, then the plan is deleted. | Phase 2 partial (H2 neutralized, not deleted); Phase 1 extraction done (#176) | | [Finish the wiring model](2026-07-30-wiring-model-completion.md) | One spine, two eyes: The Board consumes `wiring.ts`, corpus extraction (schematics, firmware, photos, CAD), facts landed with zone citations, operator answers on screen. Merges the 2026-07-27 unification, extraction, and CAD plans. | Q1/Q2 safety-relevant (wrong 40-pin numbering puts 5 V into a Jetson UART pin); X1 gates drilling | From 2800165cfd786af21160bab66694c866ddaf5dc6 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Sat, 15 Aug 2026 01:49:09 -0500 Subject: [PATCH 4/4] docs(plans): rewrite as a clean-room rebuild, not a migration The previous version was six migration phases, and it was wrong for the same reason the robot is in this state: subtraction from a bad shape cannot reach a good shape. Every migration phase has to reason about the existing system, so the old structure constrains every step -- which is precisely how 1,870 lines of shell got built, thirteen locally-correct steps at a time. The concrete tell: that draft wrote provision.sh by recording the current robot, then validated it with --check against that same robot. It would have passed trivially and enshrined every undeclared accident as a declaration. Inverts the default. The new tree starts empty; nothing is carried by inertia. The preserve list is the whole plan: * Code: beast_power alone -- 1,168 of 51,361 lines. INA219, load-compensated SoC, datasheet-vector tests, no upstream equivalent for this hardware. * Knowledge: nav2 tuning rationale, the ESP32 T-code table, the twist_mux ladder, hardware by-id paths, measured pack limits, the traps. * ugv_cockpit is flagged for an owner decision, not defaulted -- it is the Hangar bridge and the Hangar is a separate surface. * Everything else is deleted, named explicitly so nobody has to guess. Phases: extract -> build clean offline -> prove twice -> cut over and delete robot/beast/ros2_ws in one commit. C is deliberately two tests, because one cannot prove both things. C1 on the current robot proves the stack works and CANNOT prove the declaration is complete -- that machine already has everything. C2 runs provision.sh somewhere that does not, and the limit is stated honestly: a container will not catch udev rules or group memberships. Risk profile is not worse than the migration -- the new tree is built offline while the robot keeps running the old stack, and nothing is deleted before its replacement runs on hardware. Only the end state differs: designed rather than eroded. New anti-goal, and it is the load-bearing one: the preserve list does not grow during execution. "We found we still needed it" is the exact mechanism that built the current system. Co-Authored-By: Claude Opus 5 --- .../2026-08-15-beast-platform-rebuild.md | 441 +++++++++--------- docs/plans/README.md | 2 +- 2 files changed, 213 insertions(+), 230 deletions(-) diff --git a/docs/plans/2026-08-15-beast-platform-rebuild.md b/docs/plans/2026-08-15-beast-platform-rebuild.md index 25cf5184..ef98154d 100644 --- a/docs/plans/2026-08-15-beast-platform-rebuild.md +++ b/docs/plans/2026-08-15-beast-platform-rebuild.md @@ -1,296 +1,279 @@ -# BEAST-01 platform rebuild — make the repo the authority +# BEAST-01 platform rebuild — tear it down, keep a short list, build it back -**Status:** PROPOSED — 2026-08-15. Owner-requested after a session that spent its whole -budget patching symptoms. This plan is the design pass that the 2026-07-11 Pi→Orin port -implied and nobody has done. It reframes, but does not delete, the 2026-08-07 strip-down -and the 2026-08-14 vendored-surface plans: those correctly catalogue drift; this one names -why drift keeps being generated. +**Status:** PROPOSED — 2026-08-15. Owner-directed. This is a **clean-room rebuild**, not a +migration. The new tree starts empty; the default for every file, package and script is *does +not exist*, and anything that survives must be named on the preserve list with a written reason. + +It reframes but does not delete the 2026-08-07 strip-down and the 2026-08-14 vendored-surface +plans: those correctly catalogue drift. This one removes the surface that generates it. + +## Why a rebuild and not a migration + +The first draft of this plan was six migration phases. It was wrong, and the reason is worth +stating because it is the same reason the robot is in this state: + +**Subtraction from a bad shape cannot reach a good shape.** Every migration phase has to reason +about the existing system — carry it, guard it, cut over from it — so the old structure +constrains every step. That is exactly how the 1,870 lines of shell were built: thirteen +locally-correct steps, each a reasonable response to the previous one's failure. More +locally-correct steps, even subtractive ones, do not leave a local optimum. + +The concrete tell: that draft wrote `provision.sh` by recording the current robot and then +validated it with `--check` **against that same robot**. It would have passed trivially — that +machine has everything, including every undeclared accident — and enshrined them as declarations. + +A clean-room rebuild inverts the default. Nothing is carried by inertia. Every file has to be +typed on purpose, and anything we forget shows up as a failure during bring-up rather than as a +silent dependency that works until the day it doesn't. + +**Risk profile is not worse than the migration.** The new tree is built offline, alongside, while +the robot keeps running the old stack. Nothing is deleted until the replacement has run on +hardware. The difference is only that the end state is designed rather than eroded. ## The one-sentence diagnosis **The repository is not the authority on the robot, and no mechanism tells you when it is -wrong.** Everything painful is an instance of that. It is not a code-volume problem — every -line-count we measured this session (2,467 ours; ~8,300 glue; ~28,000 inert vendor) failed to -explain the pain, because the pain is structural, not dimensional. +wrong.** It is not a code-volume problem — every line count measured this session (2,467 ours; +~8,300 glue; ~28,000 inert vendor) failed to explain the pain, because the pain is structural. -Three properties, each independently sufficient to cause a bad week: - -1. **Nothing in the repo declares what runs.** `beast-install-systemd-units` copies all 17 - unit files and never runs `systemctl enable`. Which 9 of 17 start at boot exists **only** - as symlinks in `*.target.wants/` on one NVMe, created by hand across 35 days, recorded - nowhere. Restore every file from git onto a fresh disk and the robot boots running nothing, - with no document saying what should be on. +1. **Nothing in the repo declares what runs.** `beast-install-systemd-units` copies all 17 unit + files and never runs `systemctl enable`. Which 9 of 17 start at boot exists **only** as + symlinks in `*.target.wants/` on one NVMe, created by hand across 35 days. 2. **Nothing in the repo declares what the machine is.** 17 facts live only on that disk — including a one-line udev rule (`03e7`, `MODE 0666`) without which the OAK-D will not open, five group memberships, ~28 hand-installed `ros-humble-*` packages, four third-party apt - sources, and `pip install --user smbus2`, on which the entire power path depends. -3. **Mechanisms report success when they fail.** See the evidence tier below. This is the one - that makes debugging cost 10× — you never get to investigate one layer, because no layer's - green light is admissible. + sources, and `pip install --user smbus2`, on which the whole power path depends. +3. **Mechanisms report success when they fail.** Seven observed this week. This is the one that + makes debugging cost 10× — no layer's green light is admissible, so every investigation + restarts from "is any of this even true?" -## Evidence tier — what is observed vs. what is read +## Evidence tier — observed vs. read -Stated explicitly because an earlier draft of this argument blurred the two and inflated the -count by one. (`beast-ctl`'s `mask` verb failed **loudly** — a fictional capability, not a -silent failure. It does not belong on this list.) +Stated explicitly because an earlier draft blurred them and inflated the count by one. +(`beast-ctl`'s `mask` verb failed **loudly** — a fictional capability, not a silent failure. It +does not belong on this list.) **Observed firing, 2026-08-11 → 2026-08-15 (7):** | What | Evidence | | --- | --- | -| `refs/deploy/beast-01` sat 4 commits stale; the pin workflow ran **green** | GitHub API; ref at `69e411d` while `main` was at `d97a3c8` | -| `beast-slam-save` printed "not running, nothing to save" and **exited 0**, twice, with pid 5392 up | two consecutive contradictory invocations; `ins-beast-slam-save-discovery-race` | +| `refs/deploy/beast-01` sat 4 commits stale; the pin workflow ran **green** | ref at `69e411d` while `main` was `d97a3c8` | +| `beast-slam-save` printed "not running, nothing to save" and **exited 0**, twice, with pid 5392 up | two consecutive contradictory invocations | | slam_toolbox alive, `/map` publishing at 0.2 Hz, map frozen at 237×54 | `use_sim_time:=true` leak from `online_async_launch.py` | -| `map→odom` bit-identical for 3 minutes while wheel odometry accumulated ~1.0 m | TF samples; destroyed the map, wedged the costmap at cost 99 under the robot | -| Robot still turning **0.335 rad/s ten seconds** after the command ended | `odom→base_link` yaw samples; `ins-beast-latching-runaway-measured` | -| Three scripts in `/usr/local/sbin` older than the repo, fixes merged, deploy reported success | md5 diff vs `deploy/bin`; `ins-beast-runs-stale-binaries` | -| 17 units byte-identical to git; enablement exists only as symlinks | `systemctl` vs repo comparison | +| `map→odom` bit-identical for 3 minutes while wheel odometry accumulated ~1.0 m | destroyed the map; costmap wedged at cost 99 under the robot | +| Robot still turning **0.335 rad/s ten seconds** after the command ended | `odom→base_link` yaw samples | +| Three scripts in `/usr/local/sbin` older than the repo, fixes merged, deploy reported success | md5 diff vs `deploy/bin` | +| 17 units byte-identical to git; enablement exists only as symlinks | `systemctl` vs repo | -**Read in code, not observed firing (2):** `ros2 launch` returns 0 on child node death -(`return_code` set only on exception paths; ros2/launch#666 open, PR #712 unmerged); the verify's -gate restore re-arms on any `gate_before` that is not the literal string `false`, including the -empty string returned when the 8-second read times out. +**Read in code, not observed (2):** `ros2 launch` returns 0 on child node death (`return_code` +set only on exception paths; ros2/launch#666 open, PR #712 unmerged); the verify's gate restore +re-arms on any `gate_before` that is not literally `false`, including the empty string a timed-out +read returns. **Recorded by a prior session (1):** `deploy-to-beast.sh` built new code, aborted before restarting, and left `git rev-parse HEAD` reading correct. -## How it got here — the port, not a drift - -Traced through git, not assumed: +## How it got here — a port, not a drift | Date | Event | | --- | --- | | 2025-11-25 → 2026-07-10 | Waveshare `ugv_ws` (`037dfca`, DUDULRX). **Zero `deploy/`, zero systemd files** — verified against the tree | -| **2026-07-11** | First Coldaine commits. Titles: *Adapt Beast bringup for **Jetson** safety*, *Support **JetPack** OpenCV*, *Fix **Humble** rf2o metadata* — and `3777a09` **"Add disabled Jetson base service"** | -| 2026-07-12 | Storage foundation (#2–#5) | +| **2026-07-11** | First Coldaine commits: *Adapt Beast bringup for **Jetson** safety*, *Support **JetPack** OpenCV* — and `3777a09` **"Add disabled Jetson base service"** | | 2026-08-03 | `1e8a167` subtree-merges the fork into RobotOverview | | 2026-08-07 | `7e86feb` *"strip automatic safety apparatus + cmd_vel watchdog"* | | 2026-08-10 | Cockpit outage → **8 deploy files in one day** | | 2026-08-14 | Autonomy on-ramp → **13 deploy files in one day** | -122 commits total (70 pre-import, 52 post). Of the 52 touching `robot/`, **33 touch `deploy/`** — -a directory the vendor never shipped and which contains none of the robot's behaviour. +122 commits (70 pre-import, 52 post). Of the 52 touching `robot/`, **33 touch `deploy/`** — a +directory the vendor never shipped, containing none of the robot's behaviour. Day one was a **hardware port**: Raspberry Pi → Jetson Orin Nano Super 8GB (JetPack 6.2.2 / R36.5). Waveshare's SD image and their `/home/ws/ugv_ws` container path both stopped applying, -which silently transferred three responsibilities to us: - -- **the environment** — the image used to carry it → now 17 undeclared facts -- **the supervision** — a human at a terminal used to be it → now 1,870 lines of bash -- **the deploy** — the image *was* the deploy → now `beast-pull`, 647 lines - -The port was correct; you want the compute. What never happened is the sentence that should -have followed it: *"and therefore we now need a provisioning story, a supervision story, and a -deploy story."* Each got improvised instead, on the day it first drew blood. Read the script -headers in birth order and the pattern is explicit — *"Installed 2026-08-10 after the cockpit -outage"*, *"after Wi-Fi dark period"*, *"added AFTER a posegraph explosion destroyed the only -good map"*. Nobody wrote a deploy system. Thirteen times, something failed quietly and someone -wrote a script so that one thing would not fail quietly again. - -Each script is one of the vanished operator's jobs: `beast-verify` = *is it up?*; -`beast-mission` = *stop*; `beast-deploy-guard` = *don't start, it's mid-build*; `beast-pull` = -*get the new code*; `beast-link-watch` = *wifi died*; `beast-slam-save` = *save the map first*; -`beast-ctl` = *you may do that*. **1,870 lines of bash is the price of the missing human.** +silently transferring three responsibilities to us — **the environment**, **the supervision**, +and **the deploy**. The port was correct; you want the compute. The sentence that should have +followed it never got said, so each responsibility was improvised on the day it first drew blood. +Read the script headers in birth order and they admit it: *"Installed 2026-08-10 after the +cockpit outage"*, *"after Wi-Fi dark period"*, *"added AFTER a posegraph explosion destroyed the +only good map"*. + +Each script is one of the vanished operator's jobs. **1,870 lines of bash is the price of the +missing human.** + +## The preserve list + +**This is the whole plan.** Everything in `robot/beast/ros2_ws` — 51,361 lines, 32 packages, 13 +scripts, 17 units — is deleted at Phase D unless it appears below. Survival requires a reason, +and "it currently works" is not one. + +### Code that survives (1 package) + +| Item | Lines | Why it survives | +| --- | ---: | --- | +| `beast_power` | 1,168 | Genuinely ours and genuinely differentiated: INA219 on `i2c-7`, load-compensated SoC (`OCV = V + |I|·0.14Ω`), durable CSV logging with interrupted-tail repair, datasheet-vector tests. No upstream package does this for this hardware. | + +That is the list. One package, 1,168 lines, out of 51,361. + +**`ugv_cockpit` (800) needs an owner decision, not a default.** It is the Hangar bridge, and the +Hangar app is a separately deployed surface. Either it moves to the app side or it is rebuilt as +a thin `rosbridge` config. It should not survive by inertia into a robot workspace. + +### Knowledge that survives (extracted to artifacts, not code) + +Expensive to re-derive, invisible in a diff, and the actual reason a rebuild is safe: + +1. **Nav2 tuning rationale** — RotationShim wrapping RegulatedPurePursuit, `allow_reversing: + false`, `use_rotate_to_heading: true`, effective speed cap living in the velocity smoother + (`[0.26, 0.0, 1.0]`) not RPP's `desired_linear_vel`. That shape is deliberate for a robot with + a 104° blind wedge astern. Regenerating defaults silently loses it. +2. **ESP32 T-code protocol** — `T:13` drive, `T:131` startup enable (unverified), `T:132` LED, + `T:133` pan-tilt, `T:137`, `T:3` OLED, `T:900` model, inbound `T:1001` at 20 Hz. The input to + the `ros2_control` hardware interface. +3. **twist_mux ladder** — `cmd_vel_joy_robot` 150 > `cmd_vel_joy_operator` 100 > `cmd_vel_ui` 50 + > `cmd_vel_nav` 10; `locks.estop` at 255 with `timeout: 0.0`. Including the property that + expiry only *removes* a source — it never emits a zero. +4. **Hardware paths** — ESP32 at `/dev/serial/by-id/usb-1a86_USB_Single_Serial_5B5E130201-if00` + (**not** `/dev/ttyTHS1`, which docs claimed for weeks), LiDAR by-id, INA219 on `i2c-7`, udev + `03e7` for the OAK-D, groups `dialout i2c video render docker`. +5. **Pack limits** — 8.332 V measured hard trip, ~9.6 V operational floor, 0.14 Ω internal + resistance, charger 12.6 V / 2 A. Never re-derive these; an invented 9.9 V floor already + aborted a live test with ~12 minutes of reserve left. +6. **The traps** — the "Watch out for" section below, carried forward verbatim. + +Deliverable: these land as a Hangar briefing (knowledge belongs in the DB, per AGENTS.md), and +the params files in the new tree carry (1) and (3) with the rationale in comments. + +### Everything else is deleted + +Named so nobody has to guess: all 13 `deploy/bin` scripts, all 17 systemd units, the entire +vendored fork including `ugv_nav` (1,550 lines of `nav2_bringup` launch files copied verbatim +with Intel copyright headers intact, 92-line delta), `beast_base` (whose `base_ctrl.py` is +byte-identical upstream and whose job becomes the `ros2_control` hardware interface), +`ugv_bringup`, `ugv_slam`, `ugv_tools`, `ugv_vision`, `cartographer`, and the ~28,000 lines of +never-launched vendor packages. ## Done condition -All five, and none of them is a line count: - 1. A fresh Orin, flashed and given this repo, reaches a verified-running BEAST-01 with **no - undocumented step** — no tribal knowledge, no "and then you also have to…". + undocumented step**. 2. `git` states which units are enabled; changing that set is a reviewed diff, not an SSH session. -3. `provision.sh --check` fails loudly when the machine diverges from the repo, and runs in - `beast-verify`. -4. A ROS node dying makes its systemd unit fail. `systemctl is-active` is admissible evidence. -5. The workspace is built from a pinned manifest of upstream packages plus a named, justified - list of what is genuinely ours. -6. **The kill list below is executed.** `deploy/bin` is roughly 300–400 lines, not 1,870; the - unit count is roughly 8, not 17; and every surviving script has a written reason a standard - mechanism could not do its job. This is the measurable half of the done condition — the other - five are properties, this one is a number. +3. A ROS node dying makes its systemd unit fail. `systemctl is-active` is admissible evidence. +4. The workspace builds from a pinned upstream manifest plus `beast_power`. +5. `robot/beast/ros2_ws` is **deleted** — not parked, not ignored, deleted. +6. `deploy/bin` no longer exists as a concept: no supervisor scripts, no verify script, no + privilege wrapper. If any survives, it carries a written reason a standard mechanism could + not do its job. ## Phases -Ordered so that **everything that only records** precedes **everything that changes**. The -recording phases are cheap and cannot break anything, and they are the safety net for the rest — -that is the whole justification for putting them first. There is no deadline on them. The mild -structural point is that the state they capture currently has one copy, on a machine with no -backup; that is a single point of failure, not a prediction that anything is about to fail. - -### P0 — Capture, change nothing *(zero risk; do first)* +### A — Extract the preserve list *(offline + robot reads; nothing changes)* -Inputs: live robot over `ssh beast-01-ts`. +Produce the preserve list above as real artifacts: the knowledge briefing landed in Hangar, and +a short repo document naming exactly what Phase B may copy. **Nothing may be copied into the new +tree that is not named here.** This is the only gate between the old tree and the new one. -- Dump the enablement set: every unit, `is-enabled` + `is-active`, to a file in the repo. -- Dump the machine facts already inventoried (17 items) into a draft `provision.sh` that is - **not yet run** — the file is a record first, an installer second. -- Emit: `robot/beast/provision/state-2026-08-15.md` — the observed truth, dated. +Done when: a reader who has never seen the old tree can build the new one from this list. -Done when: a reader can reconstruct which services run and what the machine has, from git alone. +### B — Build the new tree, clean, offline -### P1 — The repo declares enablement +New workspace root. Empty. Every file typed on purpose: -- `beast-install-systemd-units` gains an enable/disable pass driven by a list **in the repo**. -- The list is explicit about the never-enable contract (`beast-nav` ships with no `[Install]` - section and stays hand-started; that is a decision, and it should read as one). -- Test in `tools/ci`: the declared set matches the unit files present. +- `beast.repos` — pinned upstream manifest (slam_toolbox, nav2, ldlidar, rf2o, + robot_localization, depthai, twist_mux, joy, teleop_twist_joy). Several already come from apt. +- `beast_power` — the one carried package. +- `beast_hardware` — **new code**: a `ros2_control` `SystemInterface` speaking the T-code + protocol (parse inbound `T:1001` at 20 Hz → joint states; emit `T:13` on write). This is the + riskiest item in the plan; see below. +- `params/` — nav2, twist_mux, slam_toolbox, carrying the preserved tuning with rationale in + comments. `use_sim_time` explicitly `false`, in the params file, not a launch argument. +- `systemd/` — units written fresh: direct node `ExecStart`, no `bash -lc`, no `ros2 launch` + wrapper, no `source` chain. Enablement declared in the repo. +- `provision.sh` — written **from the preserve list**, not from the robot. `--check` mode. -Done when: `systemctl enable` is never again typed by a human, and "born disabled" cannot recur. +No robot required for any of this. -### P2 — The repo declares the machine +**Fallback on the risky item:** if `beast_hardware` is not ready when the rest is, carry +`beast_base` as an explicitly temporary shim, tracked as a debt item with a written expiry — not +absorbed silently into the preserve list. -- `provision.sh`, idempotent, plus `--check`. No Ansible, no Nix, no new dependency — the bug is - *undeclared*, not *not-declared-in-a-framework*, and one robot does not justify a config - management system. -- Wire `--check` into `beast-verify` so divergence is a verify failure, not a discovery. +### C — Prove it, twice, because one test cannot prove both things -Done when: the udev rule, the groups, the apt set, `smbus2`, and linger are all in git and checked. +**C1 — functional.** Bring the new tree up on the current robot at a different workspace path, +with differently-named units, nothing enabled. Proves the stack works. **Cannot prove the +declaration is complete** — that machine already has everything, including every undeclared +accident. -### P3 — Workspace from a manifest, not a fork +**C2 — declaration completeness.** Run `provision.sh` somewhere that does *not* already have +everything: a container on the Jetson from the JetPack base image, or a fresh flash. Every +missing apt package, pip install and env assumption surfaces here, loudly. Note the limit +honestly: a container will **not** catch udev rules or group memberships, so those need a fresh +flash or explicit inspection. -The big deletion, and the one the owner has been asking for since *"what packages from ROS 2 are -we shipping?"* +Done when: C1 drives, and C2 comes up from nothing. -- Replace the 51,361-line vendored tree with a `.repos` manifest naming upstream packages at - pinned versions. Our entire net delta from the fork point is ~913 lines, most of it the - `/cmd_vel` → `/cmd_vel_nav` retarget, which becomes a remap. -- `ugv_nav` (1,550 SLOC of launch files copied verbatim from `nav2_bringup`, Intel copyright - headers intact, 92-line delta) becomes `nav2_bringup` + our params. -- **Carry forward exactly four things**, each with a written reason: - 1. `beast_power` — genuinely ours; INA219, load-compensated SoC, properly tested - 2. the `twist_mux` arbitration ladder - 3. the nav2 params (`rpp.yaml`, RotationShim, velocity smoother caps) — *tuned knowledge* - 4. the ESP32 T-code protocol table +### D — Cut over, then delete in one commit -Done when: the stack comes up from upstream packages plus those four, and the drive path is -unchanged. +Switch the robot to the new tree. Then delete `robot/beast/ros2_ws` entirely — 51,361 lines, 13 +scripts, 17 units — in a single commit. **Deleted, not migrated, not parked.** Git history is the +archive; that is what `docs/plans/README.md` already says about plans and it applies here. -### P4 — Supervision on nodes, not launch files +### E — Close out -`ExecStart=/bin/bash -lc 'source … && exec ros2 launch …'` puts systemd three levels above -anything real, watching a wrapper that returns 0 when its children die. Replace with direct node -invocation per service, one at a time, starting with `beast-slam` (the change is already drafted -on `claude/beast-slam-direct-node`). - -Done when: killing a node fails its unit. Several of the seven observed silent failures die here. - -### P5 — `ros2_control` for the drive path - -The right destination — `diff_drive_controller` supplies the `cmd_vel` timeout that -`beast-mission` currently fakes in 155 lines of trap handler, plus real joint states and odometry. -**Sequenced last on purpose:** it is the only phase that writes genuinely new code -(a `hardware_interface` plugin speaking Waveshare's JSON protocol — parse inbound `T:1001` at -20 Hz, emit `T:13` on write) against firmware we do not own. Its failure mode is "the robot does -not move," or worse, "moves wrong." - -Do not start P5 until P0–P4 are done and the robot is verifiably healthy on a known-good base. - -## The kill list - -The point of the phases is not to add a provisioning layer on top of what exists — it is to -**retire the layer that exists**. Every script below was written because a capability was -missing; the phases restore the capability, and the script goes. Stated per file so this plan -can be held to it. - -Lines are exact (`wc -l`, 2026-08-15). Survivor sizes marked *(est.)* are estimates, not -measurements — they should be treated as targets to argue with, not as facts. - -| Script | Lines | Exists because | Fate | -| --- | ---: | --- | --- | -| `beast-mission` | 155 | no `cmd_vel` watchdog (D8) | **Deleted at P5.** `diff_drive_controller` supplies the timeout as a contract | -| `beast-deploy-guard` | 36 | `--symlink-install` deploy is not atomic | **Deleted** when deploy becomes atomic | -| `beast-gamepad` | 45 | no teleop story after the port | **Deleted.** `joy` + `teleop_twist_joy` are upstream and do exactly this | -| `install-operator-shortcuts.sh` | 47 | same | **Deleted** with it | -| `beast-wifi-telemetry` | 204 | Wi-Fi drops, no evidence | **Leaves the repo.** This is UniFi forensics; it lives here only because the symptom appeared here | -| `beast-wifi-watch` | 48 | need deauth/CSA frames between samples | Leaves with it | -| `beast-link-watch` | 49 | link dies unattended | Leaves with it | -| `beast-install-systemd-units` | 59 | nothing provisions the machine | **Absorbed into `provision.sh` at P2** | -| `install-beast-sudoers.sh` | 40 | `beast-ctl` needs a root install | Absorbed at P2 | -| `beast-slam-save` | 69 | map lifecycle unmanaged; races on `ros2 node list` | Becomes a lifecycle hook on a real node, not a shell script guessing whether SLAM is up | -| `beast-verify` | 309 | systemd cannot see node health | **P4 retires most of it.** `systemctl is-active` becomes admissible; what remains is a small hardware probe (drive path, pack, i2c) — ~60 *(est.)* | -| `beast-ctl` | 162 | sudoers cannot express a policy | Shrinks once units are root-run and deploy is not user-driven — ~50 *(est.)* | -| `beast-pull` | 647 | there is no deploy system | **Re-evaluated at P5, not preserved.** An atomic deploy collapses it to fetch + swap + verify — ~150 *(est.)*. It survives P0–P4 because it works and nothing better exists yet | -| **Total** | **1,870** | | **~300–400 remaining** *(est.)* | - -Alongside it: **17 systemd units → roughly 8**, once the Wi-Fi trio (3 units + 2 timers) leaves -and the deploy pair collapses. And the vendored workspace, **51,361 lines → a ~30-line `.repos` -manifest** plus the four carried items named in P3. - -Two honesty notes on this table. First, the ~1,500 lines it retires are **load-bearing** lines — -every one is currently doing a job — which is exactly why this is worth doing and why the -inert-vendor-package deletion was not. Second, no phase may delete its script before the -replacement capability is demonstrated on the robot; a fate in this table is a commitment about -sequence, not permission to remove something early. +Delete this plan (executed plans are deleted, not archived). Update `docs/beast-ops.md` Quick +connect, `docs/deploy.md`, and `README.md`. Retire the strip-down and vendored-surface plans, +whose subject no longer exists. ## Watch out for -Traps that will bite a rebuild specifically, each verified this session: - -1. **`cartographer` is not Google Cartographer.** It is a 31-line Waveshare package that - name-shadows it, built only because `ugv_nav/localization_launch.py` calls - `get_package_share_directory('cartographer')` **unconditionally**. Park or delete it and AMCL - breaks. Fix the call before touching the package. -2. **`beast_base` is missing from both build allowlists** (`build_common.sh`, `build_first.sh`). - Already flagged as a live hazard in the vendored-surface plan. A clean rebuild from those - scripts yields a workspace where `beast-ros-base.service` fails and the startup stop never - fires — on a robot whose ESP32 latches velocity. Those scripts also hardcode - `WS=/home/ws/ugv_ws`, a container path the robot has never used. -3. **Four dependencies work only by accident.** `ugv_bringup` launches `robot_localization`, - `ugv_tools` launches `joy`, `ugv_slam` launches `slam_toolbox`/`cartographer_ros`/`rtabmap` — - none declared in `package.xml`. A manifest-based rebuild is exactly what exposes these. -4. **Tuned params look like config but are knowledge.** `rpp.yaml`'s RotationShim wrapping - RegulatedPurePursuit, `allow_reversing: false`, and the effective speed cap living in the - velocity smoother (`[0.26, 0.0, 1.0]`) rather than RPP's `desired_linear_vel` — that shape is - deliberate for a robot with a 104° blind wedge astern. Regenerating defaults silently loses it. -5. **`use_sim_time` defaults to `true`** in `online_async_launch.py` and is applied **after** the - params file, overriding it. Any path that keeps a launch file re-imports this bug. -6. **The install path cannot install.** `deploy-to-beast.sh`'s `install_bin_if_needed` falls - through to a break-glass password no unattended path can supply, prints - `WARN cannot passwordless-install`, and **continues successfully**. Three scripts are stale on - the robot right now because of it. Anything new placed in `/usr/local/sbin` inherits this. -7. **`beast-ctl` must stay root-owned in `/usr/local/sbin`.** `/home/beast` is writable by - `beast`, so allowlisting a path under it in sudoers is a trivial root escalation. It must never - be executed from the repo checkout. -8. **Never command motion from a foreground SSH process.** No `cmd_vel` watchdog exists - (owner decision D8, 2026-08-07). Any test procedure must send explicit zeros; ceasing to publish - is not stopping. Measured: 0.335 rad/s ten seconds after the command ended. -9. **In-place rotation corrupts the map.** Do not use "slow spin to build coverage" as a mapping - prerequisite — it froze `map→odom` for three minutes and wedged the costmap. Prefer translating - survey motion, and verify both that the map grew *and* that `map→odom` is moving. -10. **The verify's gate restore is fail-open** and runs on an hourly timer. -11. **`beast-mission`'s EXIT trap fires at body exit**, so a mission body that post-processes - after driving keeps driving through it. Zero immediately after the drive segment, not at exit. +Verified this session. Carried into Phase A as preserved knowledge. + +1. **`cartographer` is not Google Cartographer.** A 31-line Waveshare package that name-shadows + it, alive only because `ugv_nav/localization_launch.py` calls + `get_package_share_directory('cartographer')` **unconditionally**. Irrelevant after D, but it + will bite anyone who touches the old tree first. +2. **Four dependencies work only by accident** — `ugv_bringup`→`robot_localization`, + `ugv_tools`→`joy`, `ugv_slam`→`slam_toolbox`/`cartographer_ros`/`rtabmap`, none declared in + `package.xml`. C2 is designed to surface exactly this class. +3. **Tuned params look like config but are knowledge.** See preserve item (1). The single most + losable thing in the rebuild. +4. **`use_sim_time` defaults to `true`** in `online_async_launch.py` and is applied *after* the + params file, overriding it. It froze the map for a full session. Any design that keeps a launch + file re-imports this. +5. **`ros2 launch` returns 0 on child node death.** The root cause of the supervision gap; the + reason B uses direct nodes. +6. **No `cmd_vel` watchdog exists** (D8, 2026-08-07). Never command motion from a foreground SSH + process. Any test must send explicit zeros — ceasing to publish is not stopping. Measured: + 0.335 rad/s ten seconds after the command ended. +7. **In-place rotation corrupts the map.** Do not use "slow spin for coverage" as a mapping + prerequisite; it froze `map→odom` for three minutes and wedged the costmap. Prefer translating + survey motion and verify `map→odom` is actually moving. +8. **Wheel travel is ~55–60% of commanded** — open-loop rotation angle is untrustworthy + independently of (7). Relevant to calibrating `diff_drive_controller`. +9. **`beast-ctl` must stay root-owned in `/usr/local/sbin`** for as long as it exists. + `/home/beast` is writable by `beast`, so allowlisting a path under it is a trivial root + escalation. It must never be executed from the repo checkout. +10. **The install path cannot install.** `deploy-to-beast.sh`'s `install_bin_if_needed` falls + through to a break-glass password no unattended path can supply, prints + `WARN cannot passwordless-install`, and **continues successfully**. Three scripts are stale on + the robot right now because of it. +11. **The robot has no RTC battery** (Orin Nano carrier leaves J3 unpopulated). It cold-boots at + epoch 0 until NTP wins — ~100 s of 1969 timestamps at the 2026-08-14 boot, which killed + `beast-cockpit-serve` outright. Unit ordering in B must account for it; `wl-ds3231-rtc` is the + recorded hardware fix. ## Anti-goals -These constrain *how* the cull happens. None of them is a reason to cull less — the kill list -above is the deliverable, and a phase that ends with the same number of scripts has failed. +These constrain *how* the rebuild happens. None is a reason to preserve more. -- **No big-bang rewrite.** Nav2 autonomously drove 1.9 m and stopped 0.096 m from goal on - 2026-08-15. Every phase must leave a robot that drives, and nothing is deleted before its - replacement is demonstrated on hardware. This bounds sequence, not scope. +- **Nothing is deleted before its replacement runs on hardware.** The robot drove autonomously on + 2026-08-15 and must keep driving. This bounds sequence, not scope — Phase D deletes everything + regardless of how comfortable the old tree feels by then. - **Do not count inert code as progress.** The ~28,000 SLOC of never-launched vendor packages - (`openslam_gmapping`, `teb_local_planner`, `costmap_converter`, `ugv_voice`, …) cost nothing — - `beast-pull` rebuilds only the four always-packages plus what changed, so they are not even a - recurring build cost. They fall out of P3 for free. Deleting them is not an achievement and must - never be reported as one; the ~1,500 load-bearing lines in the kill list are the real measure. + cost nothing — `beast-pull` does not even rebuild them. They vanish with the tree at D. That is + a side effect, never an achievement, and must not be reported as one. - **No config-management framework** for one robot. The bug is *undeclared*, not *not-declared-in-Ansible*. -- **No script #17.** If the answer to a phase is "write another supervisor," the phase is wrong. - Every phase must end with fewer moving parts than it started with. - -## Verification - -- P0: a second reader reconstructs the running set from git alone, then diffs against the live - robot and finds nothing. -- P1/P2: `tools/ci` tests; `provision.sh --check` green on the live robot; deliberately break one - fact and confirm it fails. -- P3: full stack up from the manifest; `beast-verify` 15/15. -- P4: `kill -9` a node, confirm its unit enters `failed`. This is the phase's whole point. -- P5: bench-first on blocks, then the T2/T3 goal ladder, with explicit-zero stops throughout. - -## Related - -Supersedes nothing outright. Reframes the 2026-08-07 strip-down and the 2026-08-14 -vendored-surface plan: both catalogue drift correctly, and P3 removes the surface that generates -it. The autonomy on-ramp's Phase 0 (`beast-slam.service` repair) is folded into P4. +- **No script #17.** If the answer to any phase is "write another supervisor," the phase is + wrong. The new tree ends with fewer moving parts than the old one, or it has failed. +- **The preserve list does not grow during execution.** Additions require the same written + justification as the original entries, recorded in the plan. "We found we still needed it" is + the exact mechanism that built the current system. diff --git a/docs/plans/README.md b/docs/plans/README.md index 8a523324..683b6b79 100644 --- a/docs/plans/README.md +++ b/docs/plans/README.md @@ -12,7 +12,7 @@ disagree, the code is right and the plan is stale. | Plan | What it covers | Blocking? | | --- | --- | --- | -| [BEAST-01 platform rebuild](2026-08-15-beast-platform-rebuild.md) | **Governing design pass.** The repo is not the authority on the robot and nothing reports when it is wrong: enablement and 17 machine facts exist only on one NVMe, and 7 mechanisms were observed reporting success while failing. Phases: capture → declare enablement → declare the machine → workspace from a manifest → supervision on nodes → `ros2_control`. Reframes the strip-down and vendored-surface plans; folds in the on-ramp's Phase 0. | Not blocking. Carries the kill list: `deploy/bin` 1,870 lines → ~300–400, 17 units → ~8, the 51,361-line vendored tree → a manifest | +| [BEAST-01 platform rebuild](2026-08-15-beast-platform-rebuild.md) | **Governing plan — clean-room rebuild, not a migration.** The repo is not the authority on the robot and nothing reports when it is wrong: enablement and 17 machine facts exist only on one NVMe, and 7 mechanisms were observed reporting success while failing. `robot/beast/ros2_ws` (51,361 lines, 13 scripts, 17 units) is **deleted**; the preserve list is one package (`beast_power`) plus extracted knowledge. Phases: extract → build clean offline → prove twice → cut over and delete. Retires the strip-down and vendored-surface plans, whose subject ceases to exist. | Not blocking. Phase A gates everything: nothing may enter the new tree that is not on the preserve list | | [BEAST-01 Command Deck + sensor fusion](2026-07-31-beast-command-deck-plan.md) | Historical cockpit context. Its robot-side safety-spine work is superseded by the 2026-08-07 strip-down; the Hangar cockpit remains product scope. | Superseded for robot-side work | | [BEAST ROS 2 strip-down](2026-08-07-beast-ros-drift-inventory-and-stripdown.md) | Remaining BEAST ROS 2 custom-drift strip after #174: Phase 1 extracted `beast_base` from the vendor `ugv_bringup.py` and removed `/ugv/watchdog_state` consumers (**done, #176**); Phase 2 deletes vizanti + `ugv_web_app` and reverts the 12 demo retargets; Phase 3 drift audit + robot ground truth, then the plan is deleted. | Phase 2 partial (H2 neutralized, not deleted); Phase 1 extraction done (#176) | | [Finish the wiring model](2026-07-30-wiring-model-completion.md) | One spine, two eyes: The Board consumes `wiring.ts`, corpus extraction (schematics, firmware, photos, CAD), facts landed with zone citations, operator answers on screen. Merges the 2026-07-27 unification, extraction, and CAD plans. | Q1/Q2 safety-relevant (wrong 40-pin numbering puts 5 V into a Jetson UART pin); X1 gates drilling |