---
phase: 06.5-static-client-asset-split-zero-cost-client-only-fly-deploys
audit_type: verifier-audit (separate from operator signoff)
audited_at: 2026-05-16T17:05:00Z
auditor: gsd-verifier (Claude)
operator_signoff_ref: .planning/phases/06.5-static-client-asset-split-zero-cost-client-only-fly-deploys-/06.5-VERIFICATION.md
operator_verdict: approved
operator: RE
operator_signed_at: 2026-05-16T23:53:56Z
deploy_sha: 824014ae3a1792234c9654e9b907378f4c2a19e9
audit_confirms_operator: yes
status: passed
score: 6/6 must-haves verified · 3/3 requirements covered
overrides_applied: 0
gaps: []
inline_closures_acknowledged:
  - description: "Plan 04 workflow Post-swap 3-check probe lacked Bearer auth under STAGING_MODE — closed inline during Drill 2"
    commit: 0c0e055
    revalidated_in: Drill 2b (commit 585cf2d, GH Actions run 25975772140)
    status: closed
---

<!-- [doc->REQ-DEP-01] [doc->REQ-DEP-04] [doc->REQ-CLI-08] -->

# Phase 06.5 — Verifier Audit (Goal-Backward, Codebase-Grounded)

This document is a separate audit performed AFTER operator UAT signoff in
`06.5-VERIFICATION.md`. The operator's verdict (approved, RE,
2026-05-16T23:53:56Z) stands as the user-acceptance gate. This audit
independently cross-references every plan-frontmatter must-have against
the actual codebase artifacts and confirms (or refutes) the operator's
pass verdict.

**Audit verdict: CONFIRMED PASS.** All six phase-level must-haves are
verifiable in the working tree; the one inline gap surfaced during Drill 2
(workflow probe Bearer auth, commit `0c0e055`) is closed and revalidated;
all three target requirements (REQ-DEP-01, REQ-DEP-04, REQ-CLI-08) show
full required-stage coverage in `pnpm trace:check`.

---

## Phase Goal (verbatim)

> Enable zero-cost client-only deploys to Fly.io via a STATIC_ASSETS_DIR
> env-driven resolver on the server + a release-script-driven volume
> publish flow (no Docker image push for client-only changes).
> Operator-verified end-to-end on staging.

The goal has two halves: (a) static-asset split mechanism shipped in
code/config/scripts/workflow, and (b) operator-verified E2E on staging.
Both halves verified below.

---

## Must-Have Coverage (goal-backward)

| # | Truth                                                                                                           | Source     | Codebase evidence                                                                                                                                                                                                                                                                          | Audit  |
|---|------------------------------------------------------------------------------------------------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|
| 1 | Server boots and serves bundled `/app/public` when `STATIC_ASSETS_DIR` is unset, empty, or points at a missing/non-dir path | Plan 01    | `apps/server/src/static-assets.ts` lines 47-92 implements the 3-branch resolver (info-log for unset/empty + env-set+dir; warn-log + bundled fallback for missing/non-dir). Wired at `apps/server/src/index.ts:285`. 78 server unit tests pass (vitest run incl. `static-assets`).            | PASS   |
| 2 | Live symlink swap under running server changes served bytes without restart                                     | Plan 01    | `apps/server/test/static-assets.integ.test.ts` (3 scenarios, including symlink-swap mid-process via `fs.renameSync`). Drill 3 also empirically confirmed against live Fly machine (operator log: served bundle hash truly changed after `mv -T`, swap+probe 0.835s total).                  | PASS   |
| 3 | Both `fly.{staging,prod}.toml` declare `STATIC_ASSETS_DIR = "/data/client-assets/current"` + REQ-DEP-01 doc tag  | Plan 02    | `apps/server/fly.staging.toml:20`, `apps/server/fly.prod.toml:19` — exact canonical value. Header tag `[doc->REQ-DEP-01]` present on line 1 of both. `node tools/scripts/lint-fly-env.mjs` exits 0 (re-run during this audit); `pnpm lint:fly-env` wired at `package.json:61`.              | PASS   |
| 4 | `scripts/client-release.sh` validates SHA, stage-checks, atomic `mv -T` swap, readlink-protected GC, cleans up tarball | Plan 03 | 121-line bash script with all 10 required steps; `^[a-f0-9]{40}$` regex (line 47); `tar -xzf` → manifest+index sanity (lines 73-80); `ln -s` + `mv -T` (lines 91-92); `readlink -f` GC guard (lines 102-113); banned `ln -sfn` absent. 8 bats tests in `scripts/test/client-release.bats`. | PASS   |
| 5 | Two operator runbooks (ROLLBACK + UAT) ship with correct `mv -T` / `/health` conventions, no banned tokens       | Plan 03    | `docs/deploy/ROLLBACK.md` (155 lines, `[doc->REQ-DEP-04]`, 3× `mv -T`, 0× `ln -sfn`, 0× `/healthz`). `docs/deploy/uat-06.5-client-fast-path.md` (227 lines, both tags, 5 numbered drill sections).                                                                                          | PASS   |
| 6 | `deploy-staging.yml` restructured to `changes` + `full-path` + `fast-path` jobs; dorny/paths-filter SHA-pinned; 06.4 c01038f knobs survive; banned actions/tokens absent | Plan 04 | `.github/workflows/deploy-staging.yml` (495 lines). Three jobs present; `dorny/paths-filter@6852f92c20ea7fd3b0c25de3b5112db3a98da050` (40-hex SHA-pinned). Banned tokens `tj-actions/changed-files`, `ln -sfn`, `/healthz` all grep-0. All 06.4 knobs (SKIP_STAGING_VERIFY, SKIP_PHASE_4_CARRYOVER, SKIP_STAGING_SMOKE, three `[skip-*]` commit-msg strings, `cache-from type=gha,scope=rebno-staging`) grep-positive. STAGING_INVITE_TOKEN Bearer threaded through the post-swap probe (lines 324, 452, 472-473) per gap closure commit 0c0e055. | PASS   |

**Goal-half (b) — operator-verified E2E on staging:** All five drills in
`06.5-VERIFICATION.md` marked PASS by operator RE with concrete evidence
(release SHAs, log lines, GH Actions run IDs, wall-clock measurements):

- Drill 1 cold-start: machine `48e0dedbde42e8`, restart healthy ~30s,
  `static_mount_resolved source=bundled` log emit. (REQ-DEP-01 int)
- Drill 2 fast-path: 47s (2a) / 57s (2b retest after inline fix); path-filter
  routing correct; symlink + GC correct. (REQ-DEP-04 int)
- Drill 3 rollback: swap 0.468s + probe 0.367s; readlink target reached;
  served bundle hash truly rolled back. (REQ-DEP-04 int)
- Drill 4 CLI-08 fidelity: two-window walk+chat intact under fast-path
  deploy + rollback. (REQ-CLI-08 int)
- Drill 5 mixed-diff routing + 06.4 knobs: runs 25976038704 + 25976148153
  confirm full-path routing + skip-input gating. (REQ-DEP-04 int)

---

## Inline Gap Closure (acknowledged, not flagged)

| Item                                                                                | Disposition                                                  |
|-------------------------------------------------------------------------------------|--------------------------------------------------------------|
| Plan 04 workflow Post-swap 3-check `/` probe missing `Authorization: Bearer ...` under STAGING_MODE → HTTP 401 in Drill 2a | **Closed in-flight** by commit `0c0e055`. Revalidated by Drill 2b (commit `585cf2d`, GH Actions run 25975772140, 57s green). Workflow now sources `STAGING_INVITE_TOKEN` from `secrets.STAGING_INVITE_TOKEN` and conditionally attaches the Bearer header on `/` only (asset + `/health` probes stay middleware-exempt). Audit confirms code present at `.github/workflows/deploy-staging.yml:324, 452, 472-473`. Not an open gap. |

---

## Requirements Traceability (per plan frontmatter)

Phase plan frontmatter declares three requirement IDs across plans 01-05:
`REQ-DEP-01`, `REQ-DEP-04`, `REQ-CLI-08`. All three appear in
`.planning/REQUIREMENTS.md` (bare-prefix form: DEP-01 line 64, DEP-04
line 67, CLI-08 line 82) and in `traceable-reqs.toml`.

`pnpm trace:check` output (re-run by this audit, 2026-05-16):

```
[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
```

| Requirement | Description (REQUIREMENTS.md)                                                          | Required stages | Coverage in Phase 06.5                                                                                  | Audit  |
|-------------|----------------------------------------------------------------------------------------|-----------------|----------------------------------------------------------------------------------------------------------|--------|
| REQ-DEP-01  | Multi-stage Dockerfile builds Alpine/musl-compatible argon2 + better-sqlite3 (now extended in 06.5: static-asset env-resolver / cold-start fallback) | doc, impl, int  | Plan 02 doc (fly toml header tags), Plan 01 impl/unit/int (`static-assets.ts` + tests), Plan 05 Drill 1 int (cold-start) | PASS   |
| REQ-DEP-04  | GitHub Actions: push to main → build → test → deploy (extended in 06.5: fast-path job + atomic-swap release script) | doc, impl, int  | Plan 03 doc+impl (runbook + release script), Plan 04 impl+int (workflow restructure + post-swap probes), Plan 05 Drills 2/3/5 int | PASS   |
| REQ-CLI-08  | MVP gate: two players join, move, chat over deployed server (must survive fast-path deploys) | doc, int        | Plan 03 doc (UAT runbook + fast-path probe), Plan 05 Drill 4 int (operator two-window walk+chat after fast-path deploy) | PASS   |

`pnpm trace:check` overall exit code is non-zero solely due to pre-existing
parse_error / undeclared_id findings in unrelated older phase artifacts
(Phases 04, 05, 06.x — `REQ-DEP-NN`, `REQ-SRV-XX`, `REQ-X`, `REQ-CLI-XX`
placeholders + malformed tag tokens). Per CLAUDE.md "CI hard-gate is
deferred to Phase 5" and per Plan 02's documented scope-boundary rule,
these are out of scope for the 06.5 phase exit. **No `missing_stage` finding
exists for any of the three target requirements.**

No orphaned requirements: REQUIREMENTS.md does not map any additional
IDs to Phase 06.5 (this phase was inserted between Phase 6 and Phase 7
and only inherits these three).

---

## Banned-Pattern Sweep (codebase-wide on phase artifacts)

| Pattern                     | Where forbidden                                                                              | Result        |
|-----------------------------|-----------------------------------------------------------------------------------------------|---------------|
| `ln -sfn`                   | `scripts/client-release.sh`, `docs/deploy/ROLLBACK.md`, `docs/deploy/uat-06.5-client-fast-path.md`, `.github/workflows/deploy-staging.yml` | 0 matches     |
| `tj-actions/changed-files`  | `.github/workflows/deploy-staging.yml`                                                       | 0 matches     |
| `/healthz`                  | `docs/deploy/ROLLBACK.md`, `.github/workflows/deploy-staging.yml`                            | 0 matches     |

---

## Re-Verification Spot-Checks Run by This Audit

| Behaviour                                                | Command                                                                       | Result                              |
|----------------------------------------------------------|-------------------------------------------------------------------------------|-------------------------------------|
| Fly env lint enforces canonical STATIC_ASSETS_DIR value  | `node tools/scripts/lint-fly-env.mjs`                                         | exit 0; OK for both toml files      |
| Server unit + static-assets resolver tests pass          | `pnpm --filter @rebno/server test -- --run static-assets`                     | 78/78 tests passed, 13 files passed |
| Required `pnpm` script exists for lint-fly-env           | `grep -n "lint:fly-env" package.json`                                         | found at line 61                    |
| Three target reqs covered in trace:check                 | `pnpm trace:check` (filtered to phase reqs)                                   | all three `[OK]` w/ +doc +impl +unit +int |
| Inline-gap commit exists                                 | `git log --oneline 0c0e055`                                                  | "fix(06.5-04): pass STAGING_INVITE_TOKEN Bearer on fast-path / probe" |

`bats scripts/test/client-release.bats` was NOT executed by this audit
because bats is not installed on the Windows audit host (consistent with
the operator host situation documented in Plan 03 / SUMMARY 05). The
script's structural invariants are independently verified above (regex,
banned patterns, `mv -T` present, 8 `@test` blocks present); behavioural
verification falls to the operator's live-staging UAT (Drills 1-3) which
exercises the same code path against the real Fly machine, plus the
Plan 01 integration test which uses Node's `fs.renameSync` (same
POSIX `rename(2)` primitive as `mv -T`).

---

## Deviations from Plan (acknowledged in SUMMARY, no audit impact)

1. **Drill 5B consolidated under 5A** — both exercise the same
   `non_client==true → full-path` route + `SKIP_STAGING_SMOKE` env path.
   Documented; not a fail.
2. **`flyctl machine exec` substituted for `flyctl ssh console -C`** on
   Windows operator host due to "The handle is invalid" TTY error. Linux
   semantics inside the container are identical. Procedure not changed.
3. **Forward-roll deferred** — Drill 5A's full-path deploy of commit
   `824014a` naturally re-anchored the volume. Volume left at `4c24630`
   bundle (functionally identical to `585cf2d`, one-line comment delta).
   Intentional and documented.

None of these deviations affect any audit must-have.

---

## Follow-Up TODO (out of scope, surfaced to roadmap)

The operator surfaced one operator-suggested follow-up during Drill 5:
split the Playwright cli-08 smoke into a separate downstream job so
`gh run watch` can observe deploy completion independently of smoke
result. Documented in SUMMARY 05 § Follow-up TODOs as a future phase
item, NOT a gap in 06.5. Audit confirms this is correctly classified
as future scope.

---

## Audit Verdict

| Field              | Value                                                                 |
|--------------------|-----------------------------------------------------------------------|
| Goal achieved      | Yes — split mechanism present in code + operator-verified on staging   |
| Must-haves passed  | 6 / 6                                                                  |
| Requirements covered | 3 / 3 (REQ-DEP-01, REQ-DEP-04, REQ-CLI-08)                          |
| Inline gaps        | 1 surfaced + closed + revalidated (commit `0c0e055`)                   |
| New gaps surfaced  | 0                                                                      |
| Banned patterns    | All grep-0 in phase artifacts                                          |
| Operator verdict   | Confirmed                                                              |
| Final status       | **passed**                                                             |

Phase 06.5 is verified passing. Ready to proceed to next milestone phase.

---

*Verifier audit by Claude (gsd-verifier); operator signoff in
`06.5-VERIFICATION.md` remains the authoritative user-acceptance gate.*
