Principal / Head of Product + Engineering · 2026-06-19 · Refinement of Run-2, not a rewrite
Code-verified against the single 287 KB passlane/app/index.html, questions.json (323 Qs), voice-sandbox/harness.js, the vendored speech fork at passlane-ios/local-plugins/speech-recognition, and app/privacy.html. Every line citation below was re-checked this session.
Run-2's mechanical skeleton holds. Run-2's product thesis for Tier-1 does not. Two launch-blockers were missed by both prior runs, and one of them is live in the store build right now, independent of Lane.
What the code verification VALIDATES (build on these, do not relitigate):
#feedback-bar (L1631) contains #feedback-result (L1632) → #feedback-expl (L1633) → .feedback-next (L1634). feedback-expl.textContent = q.explanation at L2927 and L3003 wipes only its own children; a cx-lane sibling between L1633 and L1634 survives. Confirmed.cx-/Lane/cxListening/startCoachListening are grep-clean (0 hits). isModeUnlocked() returns true (L2151) — content is already ungated; only voice/exams/weak-drill are Pro. Lane's text tier sits inside the already-free zone. Deleting every cx- line leaves the loop byte-for-byte unchanged.startListening guards if (isExam) return locally (L3501) — startCoachListening() must re-implement it. The single-bind speech handlers (partialResults L3373, speechReady L3386) self-guard on appState !== 'listening', so they're inert during feedback autoadvance; the real collision is a Tier-3 listen sharing the 'listening' posture — which is why an explicit cxListening flag is required. The verification's correction of run-2's reason (not "appState is feedback for both" — the danger is the shared 'listening' posture) is right and I adopt it.speechPlugin.start() (L3554) carries only {language,maxResults,partialResults,popup} — no on-device flag; cloud STT today, confirmed at the native layer (Android uses createSpeechRecognizer, the online recognizer; the iOS Swift fork does not set requiresOnDeviceRecognition — it is commented out, with a note that forcing it fails when the locale model isn't downloaded, so the recognizer runs at the system default, which is cloud-capable). And the schema is exactly id/mode/category/question/choices/correct/explanation/difficulty — one explanation, no per-distractor field.What MUST change — bluntly:
advanceSeconds = 2 (L1735, slider value=2 L1423, PACING_DEFAULTS.advance L4476). In silent/text mode the bar shows, then setTimeout(advanceNow, 2000) (L3037) wipes it. Run-2's "passive source chip does not freeze the timer" rule means Lane's entire affordance flashes and dies in 2s for the default-config majority. Neither prior run stated the default. This single fact governs whether Tier-1 is worth anything.az_flagged (L1853/L1881) — a replay bookmark, not a content-error report. There is no az_reports buffer, no report button, no sink, anywhere. Google Play's generative-AI policy requires an in-app report path on any AI feature. Both runs lean on it as if built. It is vaporware and a launch-blocker for any AI tier.privacy.html L41 already makes the false claim, in the live build, before any Lane work. It reads: "PassLane itself does not record, store, or transmit any audio." The shipping answer mic round-trips audio to Apple/Google. This is a present compliance defect with zero dependency on Lane and must be hotfixed in the imminent launch, not filed under Phase 1.5.Net: keep the wiring, gut the Tier-1 content thesis, fix two things that have nothing to do with AI, and stop building the voice narrative on a privacy claim that is per-device-false.
KEPT
cx-lane as a flex sibling between L1633 and L1634; teardown in displayQuestion. Verified the only structurally safe insert; teardown is mandatory (nothing in the reset path touches a sibling — see §4).startCoachListening() re-checking isExam; reuse only NATIVE_RESTART_COOLDOWN_MS=400. The exam wall and the reopen-storm guard both demand it.cxListening flag, not appState. The core isolation rule. Verified necessary..mic-btn.listening (L1128).cx- code prefix locked. Coach/coachCtx already names the shipped reveal path (L2903/L3628) — cx- is collision-safe.STATE_FILE (AZ + TX/FL/CA/NY/NC live, else fallback to questions.json). The one Tier-1 element that adds genuinely new information.CHANGED
speechReady fallback (L3542-3549) as a THIRD answer-clock toucher. Verified it fires the haptic + noteVoiceActivity(); a separate startCoachListening() must not inherit it.max-height:30vh" → "adaptive — collapse the read explanation + voice-bar on engage, cap at min(30vh, remaining)." Verified #screen-study is 100dvh; overflow:hidden, #feedback-expl already capped at 32vh (L411) because choice D clips; a fixed 30vh stack pushes Next off a 667pt screen.#fb-text reuse: HTML idiom only, NOT its CSS. Verified .fb-text:focus is var(--blue) (L895) — reusing it paints Lane blue.openPaywall('voice'|'listen')" → "add PW_CONTEXT.ask that sells coaching." Verified voice = "study while you drive," listen = "read-aloud" (L2264-2266) — reusing them literally produces the "pay to talk" read.DROPPED
cx- stays in code, UI leads with verbs. Keep "Lane" only as a quiet internal/marketing handle, never "Hold to ask Lane" next to the "PassLane" wordmark.ADDED
az_reports local report buffer + a "Report this" control as a Phase-1 deliverable (distinct from az_flagged). Play-required; currently nonexistent.privacy.html L41 hotfix decoupled from Lane, shipped in the current launch. Live compliance defect.weakCategories()/progress[id]/the wrong letter the user picked — the net-new free value that needs zero authored prose.coach-az.json content artifact (id-keyed, src_hash-stamped, null-permitting) for any future authored layer. Keeps Lane content off the frozen, content-hashed bank so the kill-switch and edit cadence survive.#feedback-expl is already aria-live=polite (L1633) — a second polite region doubles spoken-feedback verbosity for every question.cx- placement convention (all cx- JS lives inside the existing app <script> block). So Lane code travels with the voice engine the harness checks — see §3 for why this is a one-line note, not a hardening project.P0 — do before any cx- code, foundational, cheap:
privacy.html L41 copy — ship immediately, zero review. Replace the false audio claim with the truthful "voice answering uses your device or platform speech service (Apple/Google), which may convert your speech to text on their servers; PassLane does not itself store or keep any audio." Decoupled from Lane; ship in the current launch. Effort: S.P1 — Phase-1 Lane (the integration spike), each concrete:
ti-shield-check, "AZ question bank · Disability income," scoped to STATE_FILE) + a your-data line (weakCategories() + progress[id]: "Insurable Interest — you're at 40% here") + one "Drill this topic" button routing to the existing startStudy('weak')/buildQueue filtered to q.category. Zero authored prose, all local, $0, dodges the no-reprint trap entirely. Effort: M.az_reports buffer + "Report this" control, distinct from az_flagged, writing {qid, shownText, ts} locally, surfaced via a hidden Settings copy-to-clipboard export. Effort: S.32vh explanation scroller (≈16px, not a new row); a one-shot first-session beat (reuse the az_seen_hint idiom) pauses the 2s timer once to teach provenance; thereafter passive. Effort: M.#feedback-expl's 32vh cap (it's been read) + the redundant voice-bar; give reclaimed space to cx-lane capped at min(30vh, remaining); feedback-bar is the single scroll container, .feedback-next stays flex-shrink:0. Prototype at 667pt before calling Phase 1 done. Effort: M.displayQuestion teardown of the cx-lane sibling at L2755, beside the existing voice-transcript clear. Nothing in the current reset path touches a sibling. Effort: S.cx- focus/input CSS in green→cyan (+ light-theme olive remap) — reuse the #fb-text HTML idiom, never its blue :focus. Effort: S.cx- placement convention (one-line note, not a project): put all cx- JS inside the existing app <script> block (the one containing qSilenceGen). harness.js already hard-fails loudly at L50-51 if the extracted block lacks the voice engine, so a stray <script> fails, it does not silently false-green; the only residual risk is cx- voice code landing in a different block, which this convention closes. Effort: trivial.P2 — conversion + privacy correctness (copy/config, high-leverage):
PW_CONTEXT.ask selling coaching ("You've got the why. Want to ask follow-ups out loud and talk any question through? That's Plus.") and route Lane's voice tiers through it. Effort: minutes.computeReadiness()/weakCategories()/exam scoring. Effort: hours (copy) + M (the Opus diagnostic later).P3 — voice ladder (spike-gated):
startCoachListening() with own CX_HARD_CAP_MS≈30000, own ~2.0–2.5s adaptive endpoint, own cue, no 900ms answer-fallback; branch all three answer-clock touchers on cxListening. Effort: M.Tier 1 (TEXT, free, $0, ships first). On feedback, the grounded sentence renders as today in #feedback-expl. Lane inserts a cx-lane sibling between L1633 and L1634 containing: the provenance chip (hairline footer inside the explanation scroller), the local-state teaching line, and "Drill this topic." It calls clearAdvanceTimer() only on explicit engagement; the passive render does not freeze the 2s timer (but the one-shot first-session beat does, once). displayQuestion tears it down. It transmits nothing — it ships no audio and makes no network call. This is the only tier in the first release, and it is the integration spike that proves the whole additive seam (sibling insert, teardown, brand CSS, placement convention, autoadvance timing) before any mic or authored content exists.
Tier 2 (PUSH-TO-TALK, Pro + consent, spike-gated). Press-and-hold the same control; a separate startCoachListening() window streams interim text into a Lane-owned transcript; release endpoints, finalized transcript shows, then matches the grounded bank. Inherits nothing from startListening — re-implements the isExam guard, the modal-refusal guard (L3505), and explicitly does NOT inherit the 900ms fallback.
Tier 3 (HANDS-FREE, Pro + consent, the only transmitting tier). Half-duplex, entered from Home. Loop: read → answer → clearAdvanceTimer() → Lane speaks the grounded "why" → Lane-owned "your turn" cue (never speechReady) → cx-listen → adaptive endpoint → think → answer → re-arm. Sends text only, never audio, after the consent gate. Barge-in over Lane's speech is tap-only by construction (no mic is open while Lane talks, per the .playAndRecord/duckOthers session) — "voice stop" applies only inside the user's own listen window. Do not imply a voice interrupt the architecture cannot deliver.
textContent wipes children every Q (L2927/L3003)displayQuestion does NOT touch feedback-bar siblings (L2755 hides bar only)partialResults L3373, speechReady L3386)cxListening at the top of each; when false, byte-for-byte current behavior → harness stays green.speechReady fallback (L3542-3549, fires haptic + noteVoiceActivity())startCoachListening() with its own cue; never inherit the fallback.startListening isExam guard is local (L3501)startCoachListening() re-implements if (isExam) return + the L3505 modal guard at its own entry.advanceSeconds=2 default (L1735/L4476) wipes the card in 2s#feedback-expl already aria-live=polite (L1633)aria-hidden..fb-text:focus is var(--blue) (L895)cx- focus CSS in green→cyan.lastIndexOf('<script>') over 3 tags (L44)<script> fails, not false-greens. Residual rule: keep cx- JS inside the app block (the one with qSilenceGen).az_reports / report control exists (only az_flagged bookmark, L1853/L1881/L2652)isPro() spoofable localStorage (L2148) gates all voice (canUseVoice L2174)privacy.html L41 false todayaz_reports control; the autoadvance/teardown/brand-CSS/placement-convention work; the privacy.html hotfix; the PW_CONTEXT.ask + readiness re-headline; and hands-free read-aloud of the existing explanation via the already-shipped recorded clips (eyes-busy value with zero mic risk).cx- voice handler lands in the wrong <script> block → the harness checks a block that lacks it. Guard: the inside-the-app-block convention (P1-10). Note the harness already hard-fails loudly on an empty grab (L50-51), so the dangerous case is narrow, not silent.cx-lane bleeds onto the next card (a wrong cited lesson on a licensing exam). Guard: explicit displayQuestion teardown.startCoachListening() + the three-toucher branch + a Phase-2 harness scenario asserting noteVoiceActivity() is not called and the answer haptic does not fire while cxListening.az_pro unlocks unmetered cloud STT → OS speech-service throttling degrades voice for all users (the cost vector is the OS quota, not an API bill). Guard: server receipt before any billable/transmitting call.The plugin is already a vendored fork at passlane-ios/local-plugins/speech-recognition — hand-edited Swift + Java, compiling. The spike is "add a property to code we own," not "fork from zero." But: iOS on-device needs a first-run model download (silent cloud fallback during that window — the worst moment for an honesty product), iOS 26's new DictationTranscriber drops the contextualStrings biasing the fork uses for A/B/C/D letters, and on-device accuracy is lower. So the spike's pass bar splits into two gates: (1) on-device PHRASE transcription offline (near-certain), and (2) single-LETTER A/B/C/D offline (the genuine maybe). Recast success as: on-device is the default and the code asserts it (no silent cloud fallback — if unavailable, voice is disabled, not quietly transmitted); a cold-start "preparing offline voice…" UX; a vocab-specific accuracy floor.
Is "a source chip + capped chips that can only paraphrase the one sentence on screen" worth shipping? No — and the verification proves it harder than run-2 admitted. The explanations are not stubs: 37-word median, mean 37.1, 300/323 ≥30 words. A "Put it simply" chip on a paragraph this substantial is either a reprint (forbidden) or a reword (lipstick). One empty tap on the highest-intent beat teaches "Lane is filler," permanently — the worst outcome for a trust-is-the-moat product.
A note on distractor discrimination (corrected from run-2's draft): the corpus discriminates distractors far less than first claimed. A strict detector — explanations that explicitly contrast the right answer against a named wrong concept (e.g. "This differs from life insurance, where…") — finds only ~2–8%. A loose detector that counts any contrastive/negation token ("not," "while," "rather," "however") reaches ~30%, but most of those are ordinary exposition using the word "not," not distractor teaching. The honest figure is therefore ~2–8% genuine discrimination, ~30% merely contrastive prose. This strengthens the case below: an authored "Why not B?" layer is less pre-empted by the existing corpus than a "33% already do it" claim would imply.
The minimum that makes Tier-1 genuinely worth shipping — and it needs no authored prose corpus:
STATE_FILE, with the AZ+5-state fallback). Trust, not paraphrase. Zero content cost.weakCategories(), progress[id]), a misconception pointer driven by the wrong letter they actually picked (submitAnswer already knows it), and one-tap "Drill this topic" into the existing weak-drill machinery. This is information the flat explanation cannot contain — the learner's behavior — and it's what makes a named companion feel like "a teacher who's paying attention."az_reports report control — Play-required, and the day-one escape hatch the moment Lane frames any content.The honest alternative (if even that strip is descoped): ship the provenance chip alone. It is honestly useful, offline, $0, and the front door for the voice ladder. Never ship echo chips, and never ship a disabled "Ask Lane" with a waitlist.
On the authored second corpus (the deferred, larger truth): if a future authored layer is funded, it is not a blanket 323×3 fill and not a mechanical "Why not B?" sprint across every distractor in all six live banks. The right mechanism: run the existing generate→judge pipeline (master-plan §4.4) with the judge question inverted to novelty ("does this add information NOT already in q.explanation?"). Keep only cells that pass containment AND novelty; emit null otherwise — null is the expected majority state. Store it in a separate coach-az.json (id-keyed, src_hash-stamped) so it never bloats the frozen bank, stays kill-switch-suppressible per id, and edits without a bank rebuild. This settles run-2's OQ3 empirically for single-digit dollars instead of eyeballing 2-3 samples — but it is a fast-follow, not a Phase-1 dependency.
advanceSeconds=2. It makes the entire feedback card, and therefore all of Tier-1, vanish before the default-config majority can read it. This is the most important miss; it reframes the whole "highest-intent beat" premise.az_reports/a report button as shipping; grep proves only az_flagged (a bookmark) exists. Play-required, so a launch-blocker.privacy.html L41 is already false — a live, Lane-independent compliance defect, not a future Plane-B task.#feedback-expl is already aria-live=polite; a second polite region is an accessibility regression (doubled spoken verbosity per question) for an older, partly low-vision candidate pool, not a feature.speechReady fallback is a third answer-clock toucher — a bare setTimeout, not a plugin event, so a "branch the listeners" reading misses it.voice="drive," listen="read-aloud"; reusing them for Lane's "ask" wall produces the "pay to talk" read mechanically. Neither run read the copy it proposed reusing.DictationTranscriber drops contextualStrings, and on-device has a cold-start cloud-fallback window — so "audio never leaves the device" is per-device-conditional, not binary.<html lang="en">, all-English corpus + copy, English-only on-device STT. A Spanish-first commuter is exactly the voice persona, silently excluded. Externalize Lane's new strings now (cheap insurance); defer the Spanish fork explicitly.Phase 0 — Launch hygiene (decoupled from Lane, in the current release):
privacy.html L41 copy so it is truthful against the shipped cloud answer-mic.cx- placement convention (one-line note; the harness already hard-fails loudly on a bad extraction).Gate: harness green; privacy copy truthful against the shipped answer mic; store-label work in flight but not gating the copy fix.
Phase 1a — CRAWL: provenance + report (integration spike, free, $0): the cx-lane sibling, source chip (hairline footer in the 32vh scroller), displayQuestion teardown, dedicated cx- brand CSS, adaptive vertical budget, the one-shot first-session autoadvance pause, the az_reports report control, the first-run hint + AI-disclosure-on-first-text. Prereq: P0a/P0c done. Gate: voice-sandbox/harness.js green before/after (asserts zero diff to mode_select→…→advancing); prototype at 667pt keeps .feedback-next pinned; airplane-mode smoke green; zero new /api/ calls.* This proves the additive seam with zero voice and zero authored content.
Phase 1b — the local-state teaching strip: miss-pattern line + wrong-letter misconception pointer + "Drill this topic" on existing weakCategories()/startStudy('weak'). Gate: routes correctly per category; transmits nothing.
Phase 1.5 — GATE: on-device-STT spike on real iOS + Android against IOS-VOICE-TEST-PLAN.md, split into the phrase gate and the letter gate, with the no-silent-fallback + cold-start + vocab-floor success bar. Until green: no "sends nothing" copy, PTT out of release. Content prerequisite (parallel, off the critical path): run the generate→judge novelty pass to size coach-az.json non-null cells; AZ first.
Phase 2 — WALK: hold-to-ask + the conversational wedge: separate startCoachListening(), the all-events cxListening router (incl. the 900ms fallback), PW_CONTEXT.ask, the readiness re-headline. Gate: new harness scenarios — cxListening window leaves the 5s clock + answer haptic untouched; 400ms cooldown honored on every coach stop→start; a coach utterance never calls processVoiceMatches. Mock-coaching fires 0× mid-exam.
Phase 3 — RUN: hands-free + live Pro tier: the half-duplex loop, Lane-owned cue, tap-only barge-in, the key-holding proxy, the §5.7 remote denylist, and the readiness diagnostic as the paid centerpiece. Gate: server-verified receipt → per-install token live (client flag gates UI only, cannot authorize spend); consent record + spend bind to the token; budgets + kill-switch tested; every privacy surface — in-app L1226, privacy.html, terms, paywall, Apple label, Play Data-safety, and the marketing site — flips in one coupled, gated release; first-token ≤1.5s p50 / 8s→fail-closed.
az_reports + "Report this" in Phase 1, offline queue, distinct from az_flaggedprivacy.html L41)cx- voice handler lands in the wrong <script> blockqSilenceGen); harness already hard-fails loudly on an empty grab (L50-51), so the dangerous case is narrowstartCoachListening(); branch all THREE touchers (incl. 900ms fallback); harness scenario asserts isolationcx-lane bleeds a wrong lesson onto the next carddisplayQuestion teardown at L2755az_pro → unmetered cloud STT → OS throttles voice for everyonemin(30vh, remaining); Next stays flex-shrink:0; prototype at 667ptaria-hidden.fb-text:focus paints Lane bluecx- focus CSS, green→cyan + olive light-themePW_CONTEXT.ask sells coaching; re-headline Pro on readiness; voice → a bulletcoach-az.json layer is single-digit dollars to size but carries an SME-review + light-human-pass cost per non-null cell, per state. Fund it (AZ first), defer to a fast-follow, or skip and let provenance + local data carry Tier-1? (Recommend: size it via the judge pass, then decide with real counts.)SFSpeechRecognizer+contextualStrings for letters, DictationTranscriber for conversation), or (b) accept letter-answering stays cloud-disclosed and only conversation goes on-device? Both are defensible; the choice sets the privacy copy.computeReadiness() already yields a local before/after mastery delta — we can prove "Lane works" to each user with zero analytics. Do you want even a single separately-consented, aggregates-only readiness-delta beacon for population signal, or hold the absolute no-tracking line? (Default: hold the line.)Files referenced (all absolute): /Users/arizona/CLAUDE CODE/passlane/app/index.html (lines cited verified this session), /Users/arizona/CLAUDE CODE/passlane/app/questions.json (top-level shape is {questions: [323 objects]}, each object keyed category, choices, correct, difficulty, explanation, id, mode, question; explanation 23/37/93 word min/median/max, mean 37.1, 300 ≥30 words, 0 empty; ~2–8% genuinely discriminate a named distractor, ~30% merely contrastive), /Users/arizona/CLAUDE CODE/passlane/app/privacy.html (L41 audio claim), /Users/arizona/CLAUDE CODE/passlane/voice-sandbox/harness.js (extraction L42-54, hard-fail guard L50-51), /Users/arizona/CLAUDE CODE/passlane-ios/local-plugins/speech-recognition/ (vendored fork; Swift L115-122 leaves requiresOnDeviceRecognition commented out, Android L50/L226 online recognizer), /Users/arizona/CLAUDE CODE/docs/passlane-ai-companion-MASTER-PLAN.md (run-1), /Users/arizona/CLAUDE CODE/docs/passlane-coach-interaction-wiring-SPEC.md (run-2).