Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions .qa/HANDOFF-2026-07-28-walkthroughs-motion-proof.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Handoff — 2026-07-28 · walkthroughs, CDP repair, motion-proof

Written for the next engineer. Everything below was measured, not assumed; where something
is unverified it says so. Three repos are involved, so read the **Landmines** section before
touching anything.

---

## What shipped

| Repo | PR | State |
| --- | --- | --- |
| FeatureClipStudio | [#6](https://github.com/HomenShum/FeatureClipStudio/pull/6) | **merged** 2026-07-29 |
| NodeSlide | [#110](https://github.com/HomenShum/NodeSlide/pull/110) | mergeable, waiting on one CI check |
| NodeRoom | [#244](https://github.com/HomenShum/NodeRoom/pull/244) | rebased onto main, CI re-running |

### Three product defects, all found by filming the product rather than reading it

1. **Every `FocusTrapDialog` modal rendered behind its own blur scrim.** Radix portals
overlay and content as *siblings*; the legacy scrims (`.r-modal-backdrop`,
`.r-room-modal-scrim`) were written to centre a *child*, and `FocusTrapDialog` passes
`unstyled`, which drops the Tailwind fixed/translate classes. Result: dialogs rendered
`position: static` at the end of `<body>`, under a z-95 blur — including the create-room
dialog carrying the product's one governance question. Fixed in `src/app/styles.css` by
positioning `[data-slot="dialog-content"]` itself, plus a real `.sr-only` (Tailwind's is
absent from this bundle, so the fallback `DialogTitle` printed the word "Dialog" on every
modal).
**DOM text extraction read these dialogs correctly the whole time.** Only frame review
caught it.

2. **CDP scripts were killing the developer's real browser.** `browser.close()` on a
`connectOverCDP` connection closes *Chrome*, not the socket — so every script killed the
browser the next one needed. Swept the class: 9 pure-CDP scripts drop the call;
`scripts/motion-inventory.mjs` connects *or* falls back to `launch()`, so it tracks
`weLaunchedIt` and closes only what it owns. Separately `scripts/chrome-cdp-up.ps1` used
`Stop-Process -Force`, which is what produced *"Chrome didn't shut down correctly"* and lost
tabs; it now closes gracefully with `CloseMainWindow()`.

3. **The upload verifier asserted videos that no longer exist.** `yt-verify.mjs` hardcoded two
ids that are now Private, and matched titles on a phrase the superseded clip *and its
replacement* both carry — so it could pass against the wrong video. Root cause was two
copies of the roster free to disagree. Now one `scripts/yt-roster.mjs` that both the
verifier and the privatize guard import, and the verifier checks **both directions**:
6 published must resolve, 4 superseded must be refused.

### `skills/motion-proof` — vendored into this repo on purpose

It previously lived only in `~/.claude`, which is **not version controlled**, so it could not
be handed to anyone. It now sits beside `skills/liveflow`, `skills/probe-first`, etc.

```bash
node skills/motion-proof/motion-probe.mjs # the deception corpus
node skills/motion-proof/motion-probe.mjs <url> --subject "<css>" --nudge
```

An audit found this skill **inverted in practice**: its `SKILL.md` correctly names
`Element.getAnimations()` primary and the video judge advisory, but `getAnimations()` had
**zero executable callers** while six Gemini video-judge scripts shipped. `motion-probe.mjs`
is the missing primary instrument. It ships with 7 adversarial fixtures + an honest control;
the control must pass and every deception must be caught, and running it the first time
found two false positives in the probe itself.

---

## Landmines

- **`~/.claude` is not a git repo.** `skills/motion-proof` is now vendored here, but the
*other* skills there (`design-dna`, `graph-hop`, `second-brain`, `trust-surfaces`,
`motion-ladder`) exist on one machine with no history and no backup. If they matter,
they need a home.
- **PR #244 was stacked on `codex/nodekit-contract-alignment`.** That branch's own PR (#241)
fails `NodeSlide packed consumer`, `node-platform / conformance`, and `verify` — the same
three failures #244 inherited. It has been rebased onto `main` and those failures left with
it. **Do not re-branch from `codex/*` without checking its PR is green first.**
- **Two conflicting PRs remain open in NodeRoom** — #190 (`codex/mobile-terracotta-launch`)
and #182 (`codex/proofloop-strict-live-official`), both `CONFLICTING`. Not touched; not mine.
- **`.qa/memory/findings.jsonl` had an unresolved stash conflict** (`UU`). Both sides were
distinct valid records in an append-only log, so the resolution kept the **union** — 29
records, all parsing. If you expected one side to win, check that.
- **Never render video live in a demo.** Image generation returns in seconds; video takes
minutes and fails often.
- **Deferred boot.** `boot.ts` defers the app module until first interaction, so an
unhydrated SSR shell is a *different page* from the React landing — different markup, and
the join-code control is inline rather than a dialog. Any probe or capture must nudge and
wait for a React-only element, never a timer. This is why `motion-probe` has `--nudge`.

---

## The pattern worth inheriting

The same defect appeared five times today in unrelated places: **a hand-typed value describing
a version of the artifact that no longer exists.**

- a YouTube title reading "11s walkthrough" over a 24-second video
- `METADATA.md` recording pre-recut durations
- showcase GIFs rendered from superseded captures
- the verifier's hardcoded video ids
- the roster existing twice, free to drift

Every fix was the same shape: **derive the value at write time, or keep exactly one copy.**
`yt-upload.mjs` now derives durations via `ffprobe`; `yt-roster.mjs` is the single roster.

A second, sharper version of it: **a decision recorded only in a conversation will be
re-decided by the code.** A council verdict to delete NodeSlide's Design tab as a standalone
destination is not done, is recorded nowhere in that repo, and the most recent inspector
commit reinforced the tab strip instead. Verdicts that survive must land in the repo they
govern — as a test, an invariant, or at minimum a dated note.

---

## Verification habits used here (and why)

- **Verify the claim the artifact makes, not that the artifact exists.** Frame counts, byte
sizes and HTTP 200s all passed while the clips showed another product's URL.
- **Probe gates in both directions.** A gate only ever seen passing is not known to work. The
privatize guard was tested by feeding it a keeper and confirming it refused, *before* it was
aimed at real targets.
- **Verify from the public surface.** `git push` exiting 0 is not evidence; fetch the raw URL
and grep for a content signal.
- **State coverage, don't imply it.** Artifacts carry `touched/total`, and `JOURNEYS.md`
records 10 of 13 journeys shot with the remaining three named, one of them explicitly
declined by the owner rather than missed.

---

## Open, not done

- NodeSlide #110 and NodeRoom #244 need a human to confirm the merge once CI is green.
NodeSlide's repo does not allow auto-merge, and `--admin` would bypass a branch-protection
rule that was set deliberately — so it was not used.
- `ScoreReceipt` (third link of `ReferenceObservation → DesignRule → ScoreReceipt`) was being
emitted by a parallel agent when this was written; confirm it landed and that its schema
validates the existing records unmodified.
- The 7 deception fixtures are runnable but are not yet wired into CI. They should be — that
is what turns the corpus from a demonstration into a gate.
2 changes: 2 additions & 0 deletions .qa/memory/findings.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@
{"severity":"P2","area":"mounted deck accessibility","symptom":"Two mounted workbench textareas had no accessible name","rootCause":"Placeholder text was used without an explicit label","fix":"Added aria-labels for slide comments and NodeAgent revision requests","status":"fixed","evidence":"docs/demo/nodeslide-i7-mounted-browser.receipt.json accessibility.unnamedInteractive=[]","ts":"2026-07-21T03:06:47.394Z","fp":"985afc2c40be"}
{"id":"smb-lending-tablet-tour-overlay","sev":"P1","area":"responsive-layout","symptom":"tablet-first-run-tour-obscured-primary-workspace","rootCause":"desktop-onboarding-reused-at-tablet-breakpoint","evidence":"docs/release/proof/20260721-smb-lending-room/smb-lending-tablet-1024x768.png","fix":"binder-behind-explicit-Room-toggle","status":"fixed","ts":"2026-07-21T08:07:51.402Z","fp":"da997e793aa8"}
{"id":"smb-lending-fixture-copy-drift","sev":"P1","area":"content-integrity","symptom":"agent-copy-disagreed-with-canonical-bank-statement-requirement","rootCause":"narrative-not-derived-from-fixture-label","evidence":"tests/smbLendingRoomSeed.test.ts","fix":"canonical-fixture-wording-and-seed-assertion","status":"fixed","ts":"2026-07-21T08:07:51.464Z","fp":"7c36fce153d0"}
{"severity":"P0","area":"live agent write path (post-215)","symptom":"After #215 deployed, a fresh host-owned room spreadsheet task STILL writes nothing: GLM-5.2 calls write_locked_cells on a blank sheet (nothing locked) -> output-denied -> tool_blocked; no proposal created, review queue empty. #215 correctly capped the loop (stops step 8 needs-attention vs 20+ before) but the write still fails.","rootCause":"model tool-selection error: GLM-5.2 reaches for write_locked_cells instead of update_sheet/write_cells on unlocked cells. Denial has no recovery instruction routing it to the correct tool. This is model-quality-driven, confirming the Kimi K3 flip is the actual fix not just a preference.","fix":"PRIMARY: flip default to Kimi K3 (PR #216 + convex env AGENT_ORCHESTRATOR_MODEL/AGENT_MODEL). SECONDARY (follow-up): write_locked_cells denial on unlocked cells should return a recovery instruction naming the correct write tool, and/or the tool should no-op-fallthrough to update_sheet.","status":"open","evidence":"room NRKBC9322LN; progress step 4 tool-write_locked_cells output-denied; model nebius/zai-org/GLM-5.2; qa-memory 20d943680a7e refined","ts":"2026-07-18T10:31:37.668Z","fp":"395400d4c0bf"}
{"severity":"P0","area":"live agent write path — TRUE root cause","symptom":"Fresh-room spreadsheet creation writes NOTHING, identically under BOTH GLM-5.2 and Kimi-K3. write_locked_cells returns tool_blocked failureKind=evidence_required stage=preflight_required.","rootCause":"verified_workbook_workflow guard (src/nodeagent/guardrails/workbookWorkflow.ts guardWrite) blocks any managed write until an inspect_workbook -> verify_workbook(afterWrite=false, full op set) -> write handshake establishes an approved plan. goalRequiresVerifiedWorkbookWorkflow() classifies ANY create/fill sheet-with-rows/columns goal as needing this heavyweight preflight — INCLUDING blank-sheet creation that has no existing data to protect. Neither model completes the handshake reliably (they write first, get preflight_required, do not cleanly recover), so blank-sheet creation is permanently blocked. This is the layer BENEATH what #215 fixed (A1-twin target coverage) — same deadlock family, different gate.","fix":"OPTION A (recommended): exempt genuinely blank/new sheets from the verified-workbook preflight (the workflow protects EDITS to existing/uploaded workbook data; fresh creation has nothing to verify against). OPTION B: server-side auto-run the inspect+verify preflight when a model calls write on a blank sheet (transparent recovery). OPTION C: strengthen system-prompt/tool-desc so models complete the handshake. Open question the user must confirm: is this preflight the SAME as the review-every-change wedge, or orthogonal (benchmark write-integrity vs human proposal approval)? If orthogonal, Option A is safe.","status":"open","evidence":"rooms NRKBC9322LN (GLM), NR8SN8GB3CR + NR0R5ZYG87A (kimi-k3); guardWrite preflight_required block payload captured; goalRequiresVerifiedWorkbookWorkflow matches the Q3-variance goal via rows/columns/create tokens","ts":"2026-07-18T10:43:44.401Z","fp":"84ea123117e2"}
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@ Use these files as the map:
- `src/nodeagent/skills/integration/omnigentAdapter.ts` - Omnigent YAML compatibility checks.
- `docs/NODEAGENT_ADOPTION.md` - porting checklist.
- `docs/OMNIGENT_INTEGRATION.md` - Omnigent boundary and smoke command.
- `skills/probe-first/SKILL.md` - refuse to draft outreach to a person without a verified, dated, source-backed research hook.

Rules:

- No outreach draft to a named person without a verified research hook on the
contact record. Enforce it on the tool-call path, not in a prompt; a prompt
rule loses under pressure. See `skills/probe-first/`.
- Keep writes behind `RoomTools`; do not mutate engine/backend state directly in
harness examples.
- Keep durable memory in frames/cache/job rows, not prompt transcripts or
Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -843,10 +843,26 @@ result, with step captions and a progress bar. Regenerate and judge any time wit
directly with `npm run walkthrough-review -- <feature-id> --ui-review`; lower-level
capture/render commands remain `npm run walkthroughs` + `npm run walkthroughs:render`.

### The NodeRoom walkthrough clips — three ways in

Three clips are published. Lengths are `ffprobe` readings of the source renders, not estimates:

| Clip | Length | Audio | Journey |
|---|---|---|---|
| [NodeRoom — review every agent change](https://youtu.be/3N7sBxFLFOc) | 24s | silent | R1 — the `#story` no-clobber drills |
| [NodeRoom — from landing to a room](https://youtu.be/qpzHP5-pWvw) | 17s | silent | R2 + R3 — hydrated landing → create a room → join by code |
| [NodeRoom — the full walkthrough, narrated](https://youtu.be/uvXf7e4hwt4) | 79s | local-TTS voiceover | R1 + R2 + R3 in one continuous pass |

![Fresh-user landing flow: the hydrated landing page loads, Create a room opens the "How should NodeAgent edits land?" policy choice, and the inline ENTER CODE control joins an existing room by share code](docs/walkthroughs/fresh-user-landing.gif)
<sub>The 17s fresh-user clip inline, because GitHub does not play video in a README. Rendered from the same source MP4 with this repo's two-pass palette recipe ([`scripts/walkthroughs/render.ts`](scripts/walkthroughs/render.ts): `fps=12`, 896px lanczos, `palettegen stats_mode=diff` → `paletteuse` bayer/`diff_mode=rectangle`). Filming it found and fixed a shipped regression: after the Radix migration every `FocusTrapDialog` modal rendered *behind* its own blur scrim. The hydrated React landing also turned out to be a different page from the SSR shell — join is an inline control there, not a dialog.</sub>

<sub>**Coverage, honestly.** These three cover **3 of the 6 NodeRoom journeys** on the journey map (R1 drills, R2 create-a-room, R3 join-by-code); across both products the tally is **10 of 13 journeys shot, 0 reachable and unshot**. The two unfilmed NodeRoom journeys share one root cause, not two: **R5** (in-room review/approve — the product's core journey) and **R4** (mobile approver) both need a signed-in, seeded live room, and R5 is **declined by the owner** ("no seed room") rather than missed. **R6** is not a distinct journey after probing — the `#story` "Architecture" button navigates within the page already filmed in R1. At element level the R1 capture touches **6/21 elements** on an 8,570px surface with 17 controls: a journey clip, not a control sweep.</sub>

### ▶ Full end-to-end demo — the live analyst room (narrated, with music)

The whole wedge in ~75 seconds — **Capture → Research → Brief → Evidence → Handoff** — with OpenAI TTS
narration and an original ambient music bed mixed under the voice. This is the only clip here with **audio**.
narration and an original ambient music bed mixed under the voice. This is the only clip embedded here with **audio**
(the narrated walkthrough above plays on YouTube).

https://github.com/HomenShum/noderoom/raw/main/episodes/noderoom-analyst-room-v1/renders/short.mp4

Expand Down
Loading
Loading