diff --git a/docs/tooling/driver-soup.md b/docs/tooling/driver-soup.md index c35ae24124..6ae6f7da18 100644 --- a/docs/tooling/driver-soup.md +++ b/docs/tooling/driver-soup.md @@ -248,6 +248,29 @@ git cherry-pick 64583aa7 # sendModelError only; resolve fcm imports if needed Do **not** fix this ad hoc in `~/coding/hapi/driver` during rebuild — fix the **branch tip**, then rebuild. +### Layer collisions (shared hot files) + +Some files are merged by **every** layer that touches them; **last layer wins** per hunk — there is no automatic union. + +| Hot file | Typical collision | +|---|---| +| `hub/src/sync/rpcGateway.ts` | Later layer re-merges for cursor/model work; drops RPC methods an **earlier** layer added | +| `hub/src/sync/syncEngine.ts` | May keep calls via a union repair layer (`fix/soup-sync-engine-collision`) while `rpcGateway` lost the method | +| `hub/src/web/routes/machines.ts` | REST route dropped while `syncEngine` + web client still reference it | +| `web/src/components/MarkdownRenderer.tsx` | Standalone markdown cast fixes overwritten | + +**Symptom:** `hapi-driver-rebuild --verify` red on homelab/guest even though feature branches typecheck clean in isolation. + +**Fix pattern (2026-07-04):** add a **thin collision-repair layer** on top of the manifest — do not hand-edit `~/coding/hapi/driver`: + +- `fix/soup-codex-sessions-rpc-collision` — restore `listCodexSessionsForMachine` + route +- `fix/soup-markdown-standalone-cast` — restore react-markdown component casts +- `fix/soup-sync-engine-collision` — overseer + scratchlist union on `syncEngine` + +**Prevention:** `hapi-driver-rebuild --verify` runs `hapi-soup-hotfiles-check.mjs` (syncEngine calls ⊆ rpcGateway methods). When adding a layer that edits hot files, comment in the manifest which symbols must survive lower layers. + +**Guest migration (oos-linux):** promote soup by syncing **manifest** homelab → guest, then `hapi-driver-rebuild --build-web --verify` **on guest** — do not `sync-oos-hapi-driver.sh` homelab→guest after a guest-only rebuild (overwrites composed soup). + **Bypass** (testing only): `HAPI_SKIP_DRIVER_LOCK=1`. Skips both flock and status writes; collisions corrupt the driver tree. **Why no hub API route?** The hub may be down *during* a switch — exactly when status is most wanted. File-backed status is readable when the hub is dead. diff --git a/scripts/tooling/hapi-driver-rebuild.sh b/scripts/tooling/hapi-driver-rebuild.sh index c420a172bb..594094f0f3 100755 --- a/scripts/tooling/hapi-driver-rebuild.sh +++ b/scripts/tooling/hapi-driver-rebuild.sh @@ -218,8 +218,17 @@ fi if [[ "$VERIFY" -eq 1 ]]; then echo "Running typecheck..." (cd "$DRIVER" && "$BUN" typecheck) + HOTFILES="$PRIMARY/scripts/tooling/hapi-soup-hotfiles-check.mjs" + if [[ -f "$HOTFILES" ]]; then + echo "Checking soup hot-file consistency (syncEngine vs rpcGateway)..." + "$BUN" run "$HOTFILES" "$DRIVER" + fi echo "Running tests..." (cd "$DRIVER" && "$BUN" run test) + STAMP="${HAPI_DRIVER_VERIFY_STAMP:-$HOME/.config/hapi/driver-verify-stamp}" + mkdir -p "$(dirname "$STAMP")" + git -C "$DRIVER" rev-parse HEAD >"$STAMP" + echo "Verify stamp: $STAMP ($(cat "$STAMP" | head -c 12)…)" fi echo "" diff --git a/scripts/tooling/hapi-soup-hotfiles-check.mjs b/scripts/tooling/hapi-soup-hotfiles-check.mjs new file mode 100644 index 0000000000..dd1d21e416 --- /dev/null +++ b/scripts/tooling/hapi-soup-hotfiles-check.mjs @@ -0,0 +1,42 @@ +#!/usr/bin/env bun +/** + * Soup hot-file consistency gate — run from hapi-driver-rebuild --verify. + * + * Catches layer-collision class bugs where syncEngine keeps rpcGateway calls + * but a later manifest layer dropped the matching rpcGateway method (or REST route). + * + * Usage: bun run hapi-soup-hotfiles-check.mjs [driver-dir] + */ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' + +const driver = process.argv[2] ?? process.env.HAPI_DRIVER ?? join(process.env.HOME, 'coding/hapi/driver') + +function read(rel) { + return readFileSync(join(driver, rel), 'utf8') +} + +const syncEngine = read('hub/src/sync/syncEngine.ts') +const rpcGateway = read('hub/src/sync/rpcGateway.ts') + +const calls = [...new Set([...syncEngine.matchAll(/this\.rpcGateway\.(\w+)\(/g)].map((m) => m[1]))] +const methods = new Set([...rpcGateway.matchAll(/^\s+async (\w+)\(/gm)].map((m) => m[1])) + +const missing = calls.filter((name) => !methods.has(name)) +if (missing.length > 0) { + console.error('hapi-soup-hotfiles-check: FAIL') + console.error(' syncEngine -> rpcGateway calls missing from rpcGateway:', missing.join(', ')) + console.error(' Add a collision-repair manifest layer — docs/tooling/driver-soup.md § Layer collisions.') + process.exit(1) +} + +if (syncEngine.includes('listCodexSessionsForMachine')) { + const machines = read('hub/src/web/routes/machines.ts') + if (!machines.includes('/codex-sessions')) { + console.error('hapi-soup-hotfiles-check: FAIL') + console.error(' syncEngine.listCodexSessionsForMachine but machines.ts missing GET /codex-sessions') + process.exit(1) + } +} + +console.log(`hapi-soup-hotfiles-check: OK (${calls.length} rpcGateway call site(s) checked)`)