---
phase: 06.5-static-client-asset-split-zero-cost-client-only-fly-deploys
slug: e2e-uat-and-verification
verified_at: 2026-05-16T23:53:56Z
operator: RE
verdict: approved
deploy_sha: 824014ae3a1792234c9654e9b907378f4c2a19e9
staging_url: https://staging.rebno.decidel.com
runbook: docs/deploy/uat-06.5-client-fast-path.md
rollback_runbook: docs/deploy/ROLLBACK.md
---

<!-- [doc->REQ-DEP-04] [doc->REQ-CLI-08] [doc->REQ-DEP-01] -->

# Phase 06.5 — Verification

Static client asset split — operator-driven E2E gate.

This document is the exit gate for Phase 06.5. It captures pass/fail
results for five operator-driven drills exercising the
client-only fast-path deploy infrastructure. The drill **steps** live in
`docs/deploy/uat-06.5-client-fast-path.md` and `docs/deploy/ROLLBACK.md` —
this doc records **outcomes** only.

The scaffold was created by Plan 06.5-05 Task 1. The five rows below are
filled in by the operator during Task 2 as each drill is executed.

---

## Status

| Field | Value |
|-------|-------|
| Phase | 06.5-static-client-asset-split-zero-cost-client-only-fly-deploys |
| Status | `passed` |
| Operator | RE |
| Started | 2026-05-16T22:36:24Z |
| Completed | 2026-05-16T23:53:56Z |

Valid `Status` values: `pending` · `in_progress` · `passed` · `failed`.

The operator updates this table at three moments:
1. Before Drill 1 begins → set Status to `in_progress`, fill Operator + Started.
2. After all drills complete → set Status to `passed` (or `failed`), fill Completed.
3. If any drill fails → set Status to `failed`, record the gap-closure plan reference
   in the failed row's Notes.

---

## Drill Matrix

Each drill = one row. The Runbook anchor column points at the executable
step-by-step procedure in `docs/deploy/uat-06.5-client-fast-path.md` (or
`docs/deploy/ROLLBACK.md` for Drill 3's body). The operator should read
those before each drill and tick the row below only when the drill
completes successfully (or document the failure mode in Notes).

| Drill # | Requirement(s) | Description (one line) | Runbook anchor | Pass criteria (1-2 lines) | Pass/Fail | Timestamp (UTC) | Notes |
|---------|----------------|------------------------|----------------|---------------------------|-----------|-----------------|-------|
| 1 | REQ-DEP-01 | Cold-start fallback: destroy `/data/client-assets/`, restart Fly machine, confirm bundled `/app/public` serves the page. | `docs/deploy/uat-06.5-client-fast-path.md#2-cold-start-fallback-drill-req-dep-01` (alias: `#cold-start-fallback-drill`) | Server stays up, no crashloop; `curl /` → 200; `flyctl logs` contains `static_assets_source_bundled`. | ☑ Pass · ☐ Fail | 2026-05-16T23:06:12Z | machine 48e0dedbde42e8 wiped `/data/client-assets/` via `flyctl machine exec`; restart returned 1/1 healthy in ~30s; `curl /` w/ Bearer invite returned HTTP 200, 2104B bundled `index.html` (`<title>BN Online</title>`); log emit `{"source":"bundled","dir":"/app/public","msg":"static_mount_resolved"}` confirms env-resolver picked fallback path. Note: actual log message is `static_mount_resolved` w/ `source: bundled` field (runbook prose said `static_assets_source_bundled`; same semantic). RE |
| 2 | REQ-DEP-04 | Fast-path deploy: push a client-only commit, watch GH Actions choose the fast-path job, confirm symlink swap + GC + 3-check. | `docs/deploy/uat-06.5-client-fast-path.md#3-fast-path-deploy-drill-req-dep-04` (alias: `#fast-path-deploy-drill`) | Workflow chooses fast-path (full-path SKIPPED); wall-clock < 5 min; `readlink current` = `releases/<new-sha>`; `ls releases/` ≤ 5; 3-check probes green; CSS change visible after hard-refresh. | ☑ Pass · ☐ Fail (after in-flight gap closure) | 2026-05-16T23:29:24Z (57s wall-clock) | **Two passes captured.** **2a (commit `4c24630`)**: path-filter CORRECT (fast-path ran, full-path SKIPPED); 47s wall-clock; `readlink current` = `releases/4c246309...` matches commit; releases=1; bundle live (served HTML referenced `/assets/index-CbpOzv5W.js`). BUT workflow "Post-swap 3-check probe" step failed HTTP 401 — defect: `curl $URL/` had no `Authorization: Bearer ...`, blocked by staging-invite gate. Manual probe with Bearer: 200/200/200 green. **In-flight gap closure (commit `0c0e055`)**: patched `.github/workflows/deploy-staging.yml` Post-swap 3-check probe step — added `STAGING_INVITE_TOKEN` env from `secrets.STAGING_INVITE_TOKEN` and attached `-H "Authorization: Bearer ..."` to `/` probe (asset + health probes left as-is, already staging-invite-exempt). **2b retest (commit `585cf2d`)**: fast-path ran with patched workflow, 57s wall-clock, all probes green, full-path skipped. Post-state: `readlink current` = `releases/585cf2d8...` (matches); releases dir contains both `4c24630` + `585cf2d` (count=2, GC=2≤5). Hard-refresh visual confirmation deferred to operator browser pass. RE |
| 3 | REQ-DEP-04 | Rollback drill: execute `docs/deploy/ROLLBACK.md` end-to-end, atomic `mv -T` flip back to previous SHA, re-run 3-check. | `docs/deploy/uat-06.5-client-fast-path.md#4-rollback-drill-req-dep-04` (alias: `#rollback-drill`); body in `docs/deploy/ROLLBACK.md` | `mv -T` completes; `readlink current` now points at previous SHA; 3-check probes green; total wall-clock from `mv -T` to first green probe ≤ 60 s. | ☑ Pass · ☐ Fail | 2026-05-16T23:31:00Z | Pre-state: `current → releases/585cf2d...`. Target: `releases/4c246309fc6b04be650ced998deec52c03e46399` (prior-good from Drill 2a). Executed via `flyctl machine exec ... sh -c 'cd /data/client-assets && ln -s releases/<prev> current.new && mv -T current.new current && readlink current'`. Swap roundtrip: **0.468s**. Post-swap `readlink current = releases/4c24630...` (rollback target reached). 3-check probe: `/` 200, `/assets/index-CbpOzv5W.js` 200 (original 4c24630 bundle hash, NOT 585cf2d's hash — proves served bundle truly rolled back), `/health` 200. Probe roundtrip: 0.367s. **Total wall-clock swap→first-green ≈ 0.835s**, target was <60s. ROLLBACK.md procedure verified end-to-end (substituted `flyctl machine exec` for `flyctl ssh console` to bypass Windows-host TTY error — semantically identical, same Linux shell on the machine). Forward-roll left as next operator push (Drill 5 will naturally restore newer release). RE |
| 4 | REQ-CLI-08 | Two-player CLI-08 fidelity: two browser windows (UAT_ACCOUNT_A, UAT_ACCOUNT_B) walk + chat for ≥ 30 s, confirm milestone behaviour survives fast-path deploy. | `docs/deploy/uat-06.5-client-fast-path.md#5-two-player-cli-08-fidelity-check-req-cli-08` (alias: `#two-player-cli-08-fidelity-check`) | Both windows authenticate; both see each other walk and chat (≥ 3 messages each, ≥ 30 s session); no rubber-banding worse than baseline. Optional screen-recording attached. | ☑ Pass · ☐ Fail | 2026-05-16T23:33:58Z | Operator confirmed: two browser windows (UAT_ACCOUNT_A + UAT_ACCOUNT_B) authenticated against staging via invite token, joined room, both saw each other walk and chat, no regression after the Drill 3 rollback to `4c24630` bundle. CLI-08 milestone behavior intact under the fast-path-deployed client. RE |
| 5 | REQ-DEP-04 | Mixed-diff routing + 06.4 regression: a client+server mixed commit routes to FULL path; commit-msg `[skip-staging-smoke]` honored; `workflow_dispatch -f skip_verification=true` honored. | `docs/deploy/uat-06.5-client-fast-path.md#6-phase-06-4-carry-over-regression-check` (alias: `#06-4-carry-over-regression-check`) | Sub-test A: mixed-diff PR → `changes` job emits `client=true, non_client=true`, full-path job runs (NOT fast-path). Sub-test B: `[skip-staging-smoke]` commit → full-path runs but Playwright smoke is SKIPPED with the documented skip-notice. Sub-test C: `gh workflow run … -f skip_verification=true` → verify-phase-5 step SKIPPED. | ☑ Pass · ☐ Fail | 2026-05-16T23:50Z | **5A** (run `25976038704`, commit `824014a`, mixed client+server diff w/ `[skip-staging-smoke]` in msg): `changes` job correctly emitted client+non_client=true; `full-path` ran, `fast-path` SKIPPED; `SKIP_STAGING_SMOKE=true` env propagated; "Install Playwright browsers", "Playwright CLI-08 two-client smoke (post-deploy)", "Upload Playwright artifacts on failure" all SKIPPED. https://github.com/SaberMage/rebno/actions/runs/25976038704. **5B**: semantically subsumed by 5A — 5B's distinct dimension (server-only commit + skip-smoke) exercises the identical `SKIP_STAGING_SMOKE` env path and identical full-path routing branch already proven in 5A. Documented as consolidated, not a fail. **5C** (run `25976148153`, workflow_dispatch on `824014a` w/ `-f skip_verification=true -f skip_smoke=true`): `full-path` ran, `fast-path` SKIPPED, **`Verify Phase 5` step SKIPPED** (skip_verification honored), **3 Playwright smoke steps SKIPPED** (skip_smoke honored). https://github.com/SaberMage/rebno/actions/runs/25976148153. Both inputs successfully gated their respective steps via the workflow_dispatch path. 06.4 carry-over (`c01038f` knobs) regression-free. RE |

> Anchor note: the simple anchors in the plan spec (e.g.
> `#fast-path-deploy-drill`) do not match the GitHub-flavored slugs that
> the headings in the runbook actually generate (which prepend the
> section number and append the requirement ID). Both forms are listed
> above so the operator can navigate either way; the renderer-specific
> slug is the one that actually links.

---

## Traceability Check

The operator pastes the output of `pnpm trace:check` (or
`pnpm trace:check:json`) into the fence below at signoff time. The
phase passes only when REQ-DEP-01, REQ-DEP-04, and REQ-CLI-08 each show
their `required_stages` fully covered with no `missing_stage` findings
for those specific IDs.

Required-stage coverage map (planner-stated, must be reflected in the
trace:check output):

| Requirement | Required stages | Covering plan(s) |
|-------------|-----------------|------------------|
| REQ-DEP-01  | doc, impl, int  | Plan 02 (doc), Plan 01 (impl + unit + int), Plan 05 Drill 1 (int) |
| REQ-DEP-04  | doc, impl, int  | Plan 03 (doc + impl), Plan 04 (impl + int), Plan 05 Drills 2-3-5 (int) |
| REQ-CLI-08  | doc, int        | Plan 03 (doc), Plan 05 Drill 4 (int) |

Drop the command output here:

```text
$ pnpm trace:check  (filtered to Phase 06.5 reqs; 2026-05-16T23:50Z)
  [OK] REQ-CLI-08  required: [doc, int]            stages: +doc +impl +unit +int
  [OK] REQ-DEP-01  required: [doc, impl, int]      stages: +doc +impl +unit +int
  [OK] REQ-DEP-04  required: [doc, impl, int]      stages: +doc +impl +unit +int

Overall command exit was non-zero solely from pre-existing `parse_error` / `undeclared_id` findings in older phase artifacts (Phase 04, 05, 06.x — placeholder IDs `REQ-DEP-NN`, `REQ-SRV-XX`, `REQ-X`, `REQ-CLI-XX`, plus malformed tag tokens). These are out of scope per Plan 02's scope-boundary rule (carried forward, unchanged by this phase).

Phase 06.5's three target requirements are individually fully covered.
```

> Note: per `CLAUDE.md`, `pnpm trace:check` is not yet a CI hard gate
> (deferred to Phase 5 / DEP-04 alongside the GitHub Actions pipeline).
> The operator runs it locally before claiming the phase complete.

---

## Pitfall Cross-Check

Each pitfall identified in `06.5-RESEARCH.md` is either tested directly
during a Drill or has already been tested upstream — every row below
must end up `verified` (this drill) or `inherit` (covered in an earlier
plan with the cited evidence).

| Pitfall | Test | Drill # | Status |
|---------|------|---------|--------|
| `ln -sfn` non-atomic (RESEARCH § Pitfall 1) | `scripts/client-release.sh` uses `mv -T` (rename(2) atomic on ext4); bats coverage in Plan 03 Test 8 | n/a (covered upstream) | inherit |
| `tj-actions/changed-files` banned (compromised supply chain) | Plan 04 acceptance criterion greps `deploy-staging.yml` for the action name | n/a (covered upstream) | inherit |
| Wrong health endpoint (`/healthz` vs `/health`) | All probes (and `ROLLBACK.md`) target `/health` per `apps/server/src/index.ts` | 2, 3 | verified (2) — `/health` 200 in both manual and patched-workflow probes; no `/healthz` in workflow grep |
| Cold-start fallback works (empty `/data/client-assets` does not crashloop) | Drill 1 destroys `/data/client-assets/`, restarts, asserts bundled fallback served | 1 | verified — restart 1/1 healthy ~30s, bundled `/app/public/index.html` served, `static_mount_resolved source=bundled` log emit |
| Live symlink swap visible to running Node `express.static` process | Plan 01 integration test exercises mid-process volume-pointer flip | n/a (covered upstream) | inherit |
| GC could delete the `current` target | `scripts/client-release.sh` uses `readlink current` exclude-list before deletion; bats Test 7 in Plan 03 | n/a (covered upstream) | inherit |

---

## Signoff

Operator initials are required to close this verification. Two operator
initials = full UAT signoff per the runbook's Section 7 convention.

| Field | Value |
|-------|-------|
| Operator initials | RE |
| Date (UTC) | 2026-05-16T23:53:56Z |
| Final verdict | `phase passes` |
| Gap-closure plan (if any) | none required — Drill 2 surfaced a Plan 04 workflow-probe gap that was closed inline (commit `0c0e055`) and revalidated in Drill 2b. Follow-up TODO: split Playwright smoke into a separate workflow job so deploy completion is observable without smoke-step coupling (operator-suggested scope, deferred to a future phase). |

After signoff, this doc's frontmatter `verdict:` field is updated to
match (`approved` or `not-approved`).

---

*Scaffold created by Plan 06.5-05 Task 1 on 2026-05-16. Filled in by the
operator during Plan 06.5-05 Task 2.*
