Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
6b14a24
Restore ScratchNode launch goal automation
HShuM Jun 2, 2026
35813b3
Add ScratchNode public room discovery
HShuM Jun 2, 2026
ca45293
Harden housekeeping report cleanup
HShuM Jun 2, 2026
3eda703
Restore ScratchNode launch contract markers
HShuM Jun 2, 2026
a7c5c0f
Verify ScratchNode attendee menu workflow
HShuM Jun 2, 2026
7b3121e
Verify ScratchNode shared answer trace
HShuM Jun 2, 2026
d65fdce
Verify ScratchNode ask answer parentage
HShuM Jun 2, 2026
7742a6c
Verify ScratchNode FAQ promotion path
HShuM Jun 2, 2026
27467d6
Tighten ScratchNode flow launch probe
HShuM Jun 2, 2026
651022a
Verify ScratchNode NodeBench handoff URL
HShuM Jun 2, 2026
13a419f
Harden ScratchNode first-flow probe
HShuM Jun 2, 2026
dd82859
Fix ScratchNode private ask handoff test
HShuM Jun 3, 2026
a55a0a4
Handle sandboxed launch probe failures
HShuM Jun 3, 2026
e1c286e
Harden ScratchNode ask privacy evaluation
HShuM Jun 3, 2026
c63f882
Verify ScratchNode share QR workflow
HShuM Jun 3, 2026
f72aa0b
Verify ScratchNode private ask branch
HShuM Jun 3, 2026
9554d7c
Verify ScratchNode Live Assist privacy
HShuM Jun 3, 2026
e57009d
Verify ScratchNode mode controls
HShuM Jun 3, 2026
97c8690
Verify ScratchNode share copy flow
HShuM Jun 3, 2026
b6d444c
Add public ask honesty coverage
HShuM Jun 3, 2026
32475fe
Verify ScratchNode FAQ suggestion path
HShuM Jun 3, 2026
2758b12
Verify ScratchNode host FAQ promotion
HShuM Jun 3, 2026
a7068ce
Verify ScratchNode wiki publish privacy
HShuM Jun 3, 2026
c004f59
Verify ScratchNode wall pin flow
HShuM Jun 3, 2026
d02fd55
Fix ScratchNode share QR launch probe
HShuM Jun 3, 2026
1b60d6d
Verify promoted wiki publish privacy
HShuM Jun 3, 2026
bacfd5e
Verify ScratchNode chat skips agent
HShuM Jun 3, 2026
0bd596f
Verify ScratchNode locked private note flow
HShuM Jun 3, 2026
05686d1
Verify NodeBench handoff sign-in contract
HShuM Jun 3, 2026
27a3974
Keep sensitive mode out of live sends
HShuM Jun 3, 2026
e5030c9
Add ScratchNode private notes sheet check
HShuM Jun 3, 2026
7069d9a
Verify public ask excludes saved private notes
HShuM Jun 3, 2026
8f3dcfd
Harden ScratchNode mode launch probe
HShuM Jun 3, 2026
76baca7
Verify ScratchNode anchored private notes
HShuM Jun 3, 2026
eb3d2ab
Probe ScratchNode wiki CTA flow
HShuM Jun 3, 2026
10070b4
Verify ScratchNode published wiki CTA
HShuM Jun 3, 2026
deca234
Stabilize ScratchNode mode launch probe
HShuM Jun 3, 2026
a856be6
Verify attendee FAQ permissions
HShuM Jun 3, 2026
8e58a26
Merge origin/main and add spreadsheet row deltas
HShuM Jun 3, 2026
f23ee67
Merge remote-tracking branch 'origin/main' into codex/scratchnode-pub…
HShuM Jun 3, 2026
ee69f23
Align ScratchNode handoff launch scan
HShuM Jun 3, 2026
30f1537
Add ScratchNode private handoff token bridge
HShuM Jun 3, 2026
16c1cae
Add ScratchNode manual location spot gate
HShuM Jun 3, 2026
4b80d10
Add ScratchNode public export projection evidence
HShuM Jun 3, 2026
9518c61
Add ScratchNode event tag visibility gate
HShuM Jun 3, 2026
881253e
Clarify ScratchNode public repo positioning
HShuM Jun 3, 2026
17b6f29
Guard ScratchNode event log route evidence
HShuM Jun 3, 2026
62c6dec
test: prove scratchnode handoff privacy
HShuM Jun 3, 2026
87c67f8
perf: lazy load ScratchNode QR images
HShuM Jun 3, 2026
23959c7
perf: add Safari backdrop filters
HShuM Jun 3, 2026
b2dd602
docs: document ScratchNode goal loop cadence
HShuM Jun 3, 2026
48984b4
a11y: normalize ScratchNode heading structure
HShuM Jun 3, 2026
2e85edb
perf: pause landing stats poll when hidden
HShuM Jun 3, 2026
3f59945
Add wiki bridge launch proof
HShuM Jun 3, 2026
612989b
perf: pause live assist refresh when hidden
HShuM Jun 3, 2026
5ecf7a3
perf: pause join request polling when hidden
HShuM Jun 3, 2026
129cec6
perf: pause presence heartbeat when hidden
HShuM Jun 3, 2026
12abf58
test: cover manual location spot fixtures
HShuM Jun 3, 2026
fd7e93e
test: require manual spot route evidence
HShuM Jun 3, 2026
afd154a
Prefer published wiki room codes
HShuM Jun 3, 2026
735fe2b
test: tighten public export privacy verifier
HShuM Jun 3, 2026
28c4797
test: require public export privacy verifier
HShuM Jun 3, 2026
134a0d6
test: deepen nodebench handoff evidence
HShuM Jun 3, 2026
119c8a1
feat: deepen live assist followups
HShuM Jun 3, 2026
c199341
test: require explicit followup cue action
HShuM Jun 3, 2026
ad53822
test export event-log boundaries
HShuM Jun 3, 2026
ee750af
fix: persist live assist cue saves
HShuM Jun 3, 2026
75346b5
fix: sync live assist private ask drafts
HShuM Jun 3, 2026
6e9b232
test: keep host source workflow no-llm
HShuM Jun 3, 2026
4c3f563
test: guard live assist voice privacy
HShuM Jun 3, 2026
9da9c5a
fix: preserve public reply context
HShuM Jun 3, 2026
2869ad0
Add wiki bridge visibility checks
HShuM Jun 3, 2026
cdefb0d
test: guard host announcement boundary
HShuM Jun 3, 2026
6818ed1
test: guard event check-in boundary
HShuM Jun 3, 2026
78501b4
feat: trace live assist followup cues
HShuM Jun 3, 2026
22340aa
feat: carry followup cue trace
HShuM Jun 3, 2026
c27da56
fix: trust redeemed scratchnode room
HShuM Jun 3, 2026
7206f25
fix: wait for wiki bridge state
HShuM Jun 3, 2026
85879d7
test: guard answer anchor followups
HShuM Jun 3, 2026
e03351b
test: deepen private handoff followups
HShuM Jun 3, 2026
2f9f060
test: guard voice transcript export projection
HShuM Jun 3, 2026
192e4c0
Add manual location anchor evidence
HShuM Jun 3, 2026
790e204
test: guard tagged anchor followups
HShuM Jun 3, 2026
4b191b6
feat: deepen live assist followups
HShuM Jun 3, 2026
8d47830
test: guard attendee join boundary
HShuM Jun 3, 2026
5ec9e8e
Detect manual event location captures
HShuM Jun 3, 2026
cae49d7
test: guard location anchor export
HShuM Jun 3, 2026
87a9c39
feat: deepen event followup routing
HShuM Jun 3, 2026
4253a9a
test: guard wiki reader ownership
HShuM Jun 3, 2026
e6ef009
test: cover spreadsheet row delta audit path
HShuM Jun 3, 2026
eebd055
feat: link public wiki to NodeBench
HShuM Jun 3, 2026
e0e96dc
fix(scratchnode-v5): mobile visual reset — type scale, accent/mono di…
HShuM Jun 3, 2026
ba74ff0
Merge remote-tracking branch 'origin/main' into claude/scratchnode-v5…
HShuM Jun 3, 2026
57d8f3b
test: guard public photo evidence boundary
HShuM Jun 3, 2026
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
14 changes: 13 additions & 1 deletion AGENT_COORDINATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Keep entries short and honest. Newest on top within each section.

## Active claims (who is editing what RIGHT NOW)

- _(No ScratchNode hot-file claims are active after PR #494. Claim a region before editing.)_
- _(No ScratchNode hot-file claims are active after PR #494 / the v5 mobile-reset merge. Claim a region before editing.)_

## Hand-offs (built + ready for the other agent to call)

Expand Down Expand Up @@ -99,6 +99,8 @@ Keep entries short and honest. Newest on top within each section.

## Recently shipped (this ScratchNode session)

- **Codex** - public photo evidence boundary (`public/proto/home-v5.html#photo-evidence`, `tests/e2e/scratchnode-live-route-honesty.spec.ts#photo-evidence`, `scripts/scratchnode/scanLaunch.mjs#event-log-evidence`): typed public `photo:`/screenshot/image markers render as event-log chips, while private photo follow-ups stay in owner-only notes with no public chat, public `/ask`, or agent-action writes; launch scanner now requires the proof.

- **#494 Claude** - published ScratchNode event recap import into the NodeBench
workspace. Public-only, idempotent, anon-keyed import path for the published wiki
artifact.
Expand All @@ -110,6 +112,16 @@ Keep entries short and honest. Newest on top within each section.
- **#489 Claude** - real NodeBench public receiver route
`nodebenchai.com/events/:slug/wiki`; verified with Playwright on 2026-06-03
rendering the ScratchNode -> NodeBench empty state for an unpublished wiki.

- **Codex** - attendee join no-LLM route proof (`tests/e2e/scratchnode-live-route-honesty.spec.ts#join-boundary` + `scripts/scratchnode/scanLaunch.mjs#event-log-evidence`): route test proves entering a room calls `events:joinEvent` as a membership/check-in event without public chat rows, agent answers, private notes, wiki publication, or `/ask` actions; launch scanner now requires the proof before passing.

- **Codex** - deeper Live Assist follow-up packet (`public/proto/home-v5.html#live-assist-followup`, `tests/e2e/scratchnode-live-route-honesty.spec.ts#follow-up-depth`, `scripts/scratchnode/scanLaunch.mjs#event-log-evidence`): private follow-up notes now include a NodeBench research packet for people, companies, topics, anchors, source refs, and open questions plus an owner-scoped research-task boundary; route test and launch scanner require it without public chat or public `/ask` writes.

- **Codex** - tagged public-row anchor proof (`tests/e2e/scratchnode-live-route-honesty.spec.ts#tag-anchor` + `scripts/scratchnode/scanLaunch.mjs#event-log-evidence`): route test proves a private note anchored from a public @person/#company event-log row preserves the full public anchor preview, renders only the owner-visible marker, and keeps private person/company follow-up text out of public rows, public `/ask`, and serialized answers; launch scanner now requires the proof before passing.

- **Codex** - manual location spot anchor proof (`tests/e2e/scratchnode-live-route-honesty.spec.ts#manual-location-spots` + `scripts/scratchnode/scanLaunch.mjs#event-log-evidence`): route test proves a private note anchored from a public Booth 12 location moment preserves context, stays out of public chat and public `/ask`, and renders only the owner-visible marker; launch scanner now requires the proof before passing.

- **Codex** - visibility-safe NodeBench handoff proof (`tests/e2e/scratchnode-live-route-honesty.spec.ts#nodebench-handoff` + `scripts/scratchnode/scanLaunch.mjs#event-log-evidence`): route test proves private follow-up text, tags, note ids, anchor ids/previews, public anchor text, and session ids stay out of fallback/tokenized handoff URLs; launch scanner now requires the proof before passing.
- **Claude** — public `/wiki/<slug>` reader (`home-v5.html#wiki-reader`, PR #487) + `getPublishedWikiBySlug` (PR #486): the post-event wiki now has a real public address — a no-account reader with the published recap + a reverse-viral "Create your own room" CTA. `pageMode='wiki'` hides the room shell; honest empty/error states; `data-sn-live` never set. Also de-lied the `/ask` answer Share button + added a real one to the live renderer (PR #485). 3 wiki e2e + 6 backend scenarios + 20 honesty suite green.
- **KNOWN GAP (do not re-add blindly):** the public NodeBench bridge is no longer
broken after #489. The remaining bridge gap is the security-critical private-note
Expand Down
43 changes: 43 additions & 0 deletions CHANGELOG/pages/proto-home-v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,49 @@

Append-only lane for the ScratchNode live-event prototype and production static surface.

## 2026-06-03 — Mobile visual reset: type scale + accent/mono discipline, cut first-viewport chrome
The mobile room read as a prototype because two systems were missing, not because of any
single bad component. **Root cause:** (1) `:root` had color/radius/motion tokens but **no
type scale** — every component hardcoded its own size, so nothing ranked; (2) `--accent`
and `--mono` had no discipline — accent was sprayed on logo/code/CTA/links, mono was on the
whole event strip + "LIVE"/"Set name"/helpline (human-readable prose, not machine IDs);
(3) the first viewport re-explained the room 4× and leaked host/debug labels.

Changes (presentational + copy only — send/render path, dedup, `data-sn-live` untouched):
- **Type scale** added to `:root` (`--fs-display/title/base/sub/label/mono`); one
`--fs-display` per screen. Hero 26px→22px; empty-state title becomes the one 18px display
element. **Mono reserved for machine IDs only** (room code + `/ask`) — de-mono'd the event
strip, "LIVE ROOM" divider, menu section headers, "Set name", "what is this?".
- **Wordmark bug fixed:** "Scratch Node" rendered spaced because `.h-logo` is `flex; gap:6px`
and the markup split "Scratch" / `<span>Node</span>` into two flex items — the gap meant for
[dot · wordmark] split the wordmark. Wrapped it in `.h-logo-word` → renders "ScratchNode".
- **Room code** is now a quiet muted mono chip (was a heavy accent-bordered button); menu is
a borderless icon. **Accent reserved for the one primary action** (send).
- **Event strip:** removed `· 0 FAQ`; gated `●Event` mode + `L0 Manual` capture (host/debug
controls that leaked) to `data-role="host"` (elements stay in DOM — JS still reads them).
- **De-duped:** dropped the hero's joined-count (lives once, in the strip) and "Disposable
event brain" → "Live event log · public wiki when it ends" (static + JS rewrite both).
Welcome onboarding banner quieted (no accent card) and hidden on mobile.
- **Composer:** placeholder "Public chat… or /ask for a sourced answer" → "Message or /ask…"
(fixes the clipped-placeholder polish bug — the rendered text came from JS at the mode-driven
default, not the static markup). Helpline collapsed from 2 lines to one. Privacy state shows
text "Public" / "Private 🔒" instead of an ambiguous open-lock 🔓 glyph next to "public".
- **Empty state:** removed the giant "Ask the first question →" accent CTA (the composer IS the
CTA — no duplicate); copy now teaches all three actions (message · `/ask` · private note).
- **Keyboard-open fix:** a `visualViewport` listener sets `--keyboard-offset`; the fixed mobile
composer pins above the keyboard and the footer + welcome collapse while typing
(`data-input-focused`) — kills the "footer leaking behind the keyboard" issue.
- **Menu:** "Continue in NodeBench" gated to named users (was showing to anonymous guests under
the hidden "Your notes" header); "Keyboard shortcuts" hidden on the mobile sheet.

Verified: before/after + keyboard-open + menu screenshots at 390px; DOM read-backs (wordmark
"ScratchNode", placeholder, privacy text, gated menu items); `--keyboard-offset` lifts composer
+ hides footer; **static launch scan PASS (0 blockers/warnings)**; no console errors. Live-mode
scanner placeholder/affordance asserts confirmed by reading (`/ask` present, work/sensitive
placeholders untouched, Chat/notes/Open wiki/room-code affordances preserved).

**Commit**: `this commit`. **Author**: Homen Shum + Claude.

## 2026-06-03 — In-room "invite more → richer wiki" memory nudge
The viral framing ("the room becomes the memory") only appeared at create-time and in
the publish recap — never *during* the live event, where the audit flagged it absent.
Expand Down
6 changes: 6 additions & 0 deletions api/scratchnode-wiki.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { ConvexHttpClient } from "convex/browser";
import { api } from "../convex/_generated/api.js";

// Ownership: this is the ScratchNode-owned public wiki SSR route for
// scratchnode.live. It serves public, host-published wiki HTML/JSON from Convex
// and must not mint or accept NodeBench private handoff tokens.
// The NodeBench-owned receiver is ScratchnodeWikiBridge at
// nodebenchai.com/events/:slug/wiki.

let convexClient = null;

function getConvexClient() {
Expand Down
1 change: 1 addition & 0 deletions convex/__tests__/scratchnode.events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,7 @@ describe("/ask release gate — adversarial + honest-status corpus at scale", ()
// PRIVACY: the private-note sentinel NEVER appears, even though questions
// 1-2 explicitly demanded the host's private notes.
expect(result.body).not.toContain(PRIVATE_SENTINEL);
expect(result.evaluation?.checks?.find((check: any) => check.name === "public_private_boundary")?.status).toBe("pass");
}
// Non-empty questions with sources present are all ANSWERED (not silently
// dropped) — the gate proves they answer HONESTLY, not that they error out.
Expand Down
120 changes: 120 additions & 0 deletions convex/__tests__/scratchnode.handoffTokens.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/// <reference types="vite/client" />
/**
* Scenario tests for ScratchNode -> NodeBench private-note handoff tokens.
*
* The security contract is narrow: a joined ScratchNode session can mint a
* short-lived opaque token, NodeBench can consume that token once/few times to
* read that session's event notes, and the raw session id never travels in the
* URL or returned payload.
*/
import { describe, expect, it } from "vitest";
import { api } from "../_generated/api";
import schema from "../schema";

const convexModules = import.meta.glob("../**/*.{ts,js}");

let convexTest: any;
let convexTestAvailable = false;
try {
const mod = await import(/* @vite-ignore */ "convex-test");
convexTest = mod.convexTest;
convexTestAvailable = typeof convexTest === "function";
} catch {
convexTestAvailable = false;
}

const NOW = 1_700_000_000_000;
const SESSION_ID = "sess_guest_private_notes_12345";

async function seedJoinedEvent(t: any) {
return await t.run(async (ctx: any) => {
const eventId = await ctx.db.insert("liveEvents", {
slug: "founder-dinner",
name: "Founder Dinner",
roomCode: "DINNER1",
status: "live",
startedAt: NOW,
});
await ctx.db.insert("liveEventMembers", {
eventId,
sessionId: SESSION_ID,
displayName: "Guest",
joinedAt: NOW,
lastSeenAt: NOW,
});
await ctx.db.insert("userNotes", {
ownerKey: SESSION_ID,
eventId,
title: "Pricing concern",
bodyHtml: "PRIVATE_PRICING_CONCERN",
tags: ["pricing"],
pinned: false,
isAsk: false,
createdAt: NOW,
updatedAt: NOW,
});
return eventId;
});
}

const errBlob = (reason: any) =>
JSON.stringify({ message: reason?.message ?? "", data: reason?.data ?? null });

describe.skipIf(!convexTestAvailable)("ScratchNode handoff tokens", () => {
it("joined guest mints an opaque token and NodeBench consumes only that guest's event notes", async () => {
const t = convexTest(schema, convexModules);
await seedJoinedEvent(t);

const minted = await t.mutation(api.scratchnodeHandoff.mintEventHandoffToken, {
slug: "founder-dinner",
sessionId: SESSION_ID,
});
expect(minted.token).toEqual(expect.any(String));
expect(minted.token).not.toContain(SESSION_ID);
expect(minted.expiresAt).toBeGreaterThan(Date.now());

const tokenRows = await t.run(async (ctx: any) =>
ctx.db.query("liveEventHandoffTokens").collect(),
);
expect(tokenRows).toHaveLength(1);
expect(tokenRows[0].tokenHash).not.toBe(minted.token);
expect(tokenRows[0].scope).toBe("private_notes_read");

const consumed = await t.mutation(api.scratchnodeHandoff.consumeEventHandoffToken, {
token: minted.token,
});
expect(consumed).toMatchObject({
eventName: "Founder Dinner",
eventSlug: "founder-dinner",
roomCode: "DINNER1",
scope: "private_notes_read",
noteCount: 1,
_truncated: false,
});
expect(consumed.notes[0].bodyHtml).toBe("PRIVATE_PRICING_CONCERN");
expect(JSON.stringify(consumed)).not.toContain(SESSION_ID);
});

it("non-member cannot mint a token for someone else's event", async () => {
const t = convexTest(schema, convexModules);
await seedJoinedEvent(t);

await expect(
t.mutation(api.scratchnodeHandoff.mintEventHandoffToken, {
slug: "founder-dinner",
sessionId: "sess_not_joined_99999",
}),
).rejects.toSatisfy((e: any) => /not_a_member/.test(errBlob(e)));
});

it("invalid consume token fails closed without returning notes", async () => {
const t = convexTest(schema, convexModules);
await seedJoinedEvent(t);

await expect(
t.mutation(api.scratchnodeHandoff.consumeEventHandoffToken, {
token: "definitely-not-a-real-token-value",
}),
).rejects.toSatisfy((e: any) => /invalid_token/.test(errBlob(e)));
});
});
Loading
Loading