Skip to content

Onboarding fixes + docs staleness (#829, #830) - Test Plan โ€‹

FieldValue
Branch / PRdocs/onboarding-fixes / PR #831
IssuesCloses #829; #830 (publish command); split-out follow-up #833
EnvLocal worktree on the dev VM (Linux). No deploy surface.
Date2026-08-09

North star โ€‹

A real newcomer (macOS or WSL, no credentials, no cloud access) can go from "I got a GitHub invite" to a running app without being told they are blocked, and without being handed a command that fails or installs a stranger's package.

Most of this PR is documentation, which is verified by reading. The parts with a runtime surface are the ones below, and they are the ones actually exercised.

Scenarios โ€‹

1. The CLI entry point resolves and runs โ€‹

  • Do: ./lantern doctor from the repo root.
  • Expected: full check runs; no reliance on npx or a global install.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    Runs the full 8-check report and exits 0 ("Environment looks good").
Evidence:  New root symlink `lantern -> tooling/scripts/lantern.mjs` committed as mode 120000.

2. The rejected CLI forms really are broken (verify the advice, not just write it) โ€‹

  • Do: run npm run lantern -- doctor from a subdirectory (apps/web).
  • Expected: fails, which is why it is not the documented form.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    `npm error Missing script: "lantern"` from apps/web. npm resolves the nearest
           workspace package.json, which has no such script. Confirms the docs' claim.
Evidence:  Also confirmed there is no node_modules/.bin/lantern, so a bare `lantern`
           genuinely requires the one-time `npm link`; documented as optional, not automated.

3. New doctor check for lantern on PATH is informational, not a failure โ€‹

  • Do: ./lantern doctor on a machine with no link.
  • Expected: the row appears with the exact npm link hint, and the overall run still passes.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    Prints "โš  lantern on PATH / not linked, use `./lantern <cmd>` from the repo root"
           with the hint, and the summary still reads "Environment looks good."
Evidence:  Severity is `optional`, so it cannot set requiredFailed.
Update:    2026-08-09, operator test finding: `npm link` PROMOTED from optional aside to a
           documented DAY_ONE setup step (step 1, right after `npm install`), so the bare
           `lantern <cmd>` is the taught form. Updated: DAY_ONE, DEVELOPER_ONBOARDING, the
           postinstall banner (both branches, re-rendered and column-checked), the doctor
           row hint, and the AGENTS.md routing rule (synced). The doctor row STAYS severity
           `optional` (a permission-walled link must not fail doctor) and the link is still
           not automated (postinstall must not mutate the global npm prefix). The banner was
           re-verified in the unlinked mock clone; the doctor not-linked hint string is
           verified by reading only, since this VM has `lantern` linked machine-wide.

4. Postinstall banner renders and no longer misleads โ€‹

  • Do: run the banner directly (both branches of its copy).
  • Expected: shows the ./lantern form, leads with running the app, and does not present secrets as a setup step.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    folderOpen branch renders correctly with aligned columns:
           1. npm install / 2. ./lantern doctor / 3. npm run dev (no secrets needed).
Evidence:  Before this PR it said `npx lantern doctor` then `npm run env:bootstrap`, i.e. the
           first thing a newcomer read told them to pull secrets and never mentioned `npm run dev`.
Caveat:    The post-install (non-folderOpen) branch self-suppresses when `.env.local` exists, which
           it does on this VM, so that branch was verified by reading, not executed. Its column
           alignment was checked by character count.

5. .vscode/extensions.json can actually ship โ€‹

  • Do: create the file, then confirm git will track it.
  • Expected: staged, not ignored.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    `git status` shows `A  .vscode/extensions.json`.
Evidence:  Required a .gitignore change: `.vscode/` was fully ignored, and a negation cannot
           rescue a file out of an ignored DIRECTORY, so it is now `.vscode/*` + `!.vscode/extensions.json`.
           Without this the fix would have silently shipped nothing.

6. Dev Containers prompt is actually suppressed โ€‹

  • Do: open the repo in VS Code on a machine without Docker and see whether the "install Dev Containers" prompt appears.
Result:    [ ] pass   [ ] fail   [x] NOT VERIFIED (needs a real VS Code)
Actual:    Cannot be exercised on this headless VM. `unwantedRecommendations` is documented to
           suppress workspace recommendations, but whether it suppresses VS Code's CONFIG-BASED
           devcontainer suggestion (triggered by detecting .devcontainer/, not by this file's own
           recommendations array) is untested.
Next:      Whoever opens the repo in VS Code next confirms. If the prompt still fires, the fallback
           is `"extensions.ignoreRecommendations": true` in .vscode/settings.json, documented inline
           in extensions.json along with why remote.containers.showReloadPrompt cannot work here.

7. Google Doc publisher (#830) โ€‹

  • Do: npm run docs:publish-onboarding -- --dry-run, then the real publish.

SUPERSEDED, read this first. The "pending one operator command" framing below is no longer true. She ran it and Google refused at the consent screen: auth/drive is a RESTRICTED scope and gcloud's built-in OAuth client is not verified for it ("This app is blocked"). Re-running cannot fix it. The other option, auth/drive.file, is permitted but can only reach files the script itself created, so it can never touch the hand-made Doc. Both ends of the fork are closed, so the live publish is not blocked on a command, it is blocked on a DESIGN DECISION that is the operator's (#830 remains open): (a) let the script create and own its Doc, (b) use a service account with the Doc shared to it, or (c) drop the sync entirely and make the Doc canonical and hand-edited with the repo just linking to it, mirroring how the business plan already works. A requirement discovered late cuts across all three: she wants the Doc to stay editable, and the real obstacle there is this script's ONE-WAY overwriting sync, not the scope. If (c) wins, the script is deleted rather than fixed. Nothing about the rest of this PR depends on the outcome.

Result:    [x] dry-run pass   [ ] live pass   [x] live BLOCKED (on a design decision, not a command)
Actual:    Dry run renders correctly: 3684 bytes of HTML, one-way-sync banner, headings, lists,
           bold and code spans all correct, target URL printed, nothing sent.
           The LIVE PATCH was attempted against the real file ID and returned
           HTTP 403 ACCESS_TOKEN_SCOPE_INSUFFICIENT. So the failure path is proven end to end
           (loud, actionable message; nothing half-written), and the SUCCESS path is unverified.
Blocked on: this VM's ADC lacks the Drive scope. The fix is an interactive browser login only the
           operator can complete:
             gcloud auth application-default login \
               --scopes=https://www.googleapis.com/auth/drive.file,https://www.googleapis.com/auth/cloud-platform
           NOT run by an agent on purpose: it REPLACES the ADC for every session on this box, and
           other sessions depend on it for Firestore admin access. Because the command includes
           cloud-platform alongside drive.file it should be additive rather than a downgrade, but
           that is her call to make knowingly.
Then:      `npm run docs:publish-onboarding` and confirm content at
           https://docs.google.com/document/d/1nbP9FyeInh_6-jrBr769-aMffKoTj18WREtr7eaNE10/edit
Interim:   2026-08-09, for today's onboarding: the operator hand-pastes the content from a
           paste-ready render of BEFORE_YOU_START.md (repo paths turned into full GitHub URLs,
           no one-way-sync banner since no sync exists). The #830 design decision stays open.
           Sharing check: an anonymous export of the Doc returned HTTP 401, so link-sharing
           was still off as of this morning; the operator flips it before handing out the URL.
RESOLVED:  2026-08-09, operator decision: no Google Doc at all. The dev admin portal's Docs
           tab (the VitePress embed, rebuilt on every dev deploy) is the out-of-repo reader:
           grant a newcomer an admin-dev login and point them at Docs > Guides. The publisher
           script is DELETED per its own STATUS header (option c), its npm script and help
           entry removed, and the docs.google.com CSP-scanner exclusion reverted. This
           scenario is retained as the record of why no publish path exists. Closes #830.

8. The pre-PR gate โ€‹

  • Do: npm run validate (full, 33 checks).
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    First run: 31 pass / 2 FAIL, both real and both mine.
           - Em Dash Check: venueService.js at 28 vs a baseline of 27. The extra was a comment
             written in #816 that reached dev. Fixed the character (the rule is "do not write
             them", not "grandfather them"), then re-snapshotted to lock in a reduction.
           - Header Validation: CSP scanner flagged `docs.google.com` as missing from the app's
             connect-src. Adding it there would have been WRONG (the app never connects to Google
             Docs); it comes from a console.log in the new manual publish script, so it was added
             to EXCLUDED_DOMAINS beside the existing cloud.google.com/github.com entries.
           Both re-run green.
Gotcha:    The background runner reported "exit code 0" because the command ended in `| tail`,
           so the pipeline's status masked validate's. The failure was caught by reading the
           summary table. Do not trust an exit code from a piped background validate.

9. Code review round โ€‹

  • Do: /code-review on the full diff, then re-verify every executable surface after applying fixes.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    11 findings; 10 applied, 1 declined with reason. Two were substantive:
           - The documented Google auth scope was `drive.file`, which CANNOT reach a Doc created
             by hand in the Drive UI. The operator's first real publish would have failed no
             matter how many times she re-authed. Corrected to the full `auth/drive` scope.
           - The markdown renderer silently mangled code fences, links and ordered lists. Since
             the sync overwrites one-way and DAY_ONE tells editors to mirror content into the
             source file, corrupted output could have reached the new-hire Doc with no error.
             Now hard-fails naming the construct.
           Also caught a FALSE CLAIM I had written into five places: "npm run lantern fails from
           any subdirectory". It works from `docs/` and fails only inside a WORKSPACE directory.
           Verified both directions myself before correcting.
Re-verified after fixes: `./lantern doctor` (green), postinstall banner (renders), publish
           dry-run (3684 bytes), `sync:agents` (11 rules), em dash (green), header validation
           (green), nested `.vscode/` ignored again while the root file stays tracked, and the
           markdown guard proven to both pass the real file and reject fences/links.
Declined:  the dead `--folder-open` banner branch. Pre-existing behaviour this PR only reworded;
           deleting it without being able to exercise a VS Code task is the larger risk.

10. Fresh-clone mock onboarding (full DAY_ONE walkthrough, day-of dry run) โ€‹

  • Do: clone the branch into a clean directory outside the checkout, then follow DAY_ONE literally: npm install, ./lantern doctor, npm run dev, open the app in a browser.
  • Expected: running app with zero credentials, and every "this output is expected" claim in the docs matches what actually prints.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    Ran 2026-08-09 ~10:30 PDT, one hour before the real newcomer's start.
           - `npm install`: 2m, exit 0. Printed exactly the noise DAY_ONE says to ignore
             (EBADENGINE from the functions workspace, deprecations, 39-vulnerability audit
             line). Postinstall banner leads with `./lantern doctor` then `npm run dev
             (no secrets needed)`; secrets correctly framed as later-only.
           - `./lantern doctor`: exits 0, "Environment looks good." Yellow `.env.local` and
             `lantern on PATH` rows as documented. (gcloud rows are green on this VM because
             it has credentials; a real newcomer sees the documented yellow there too.)
           - `npm run dev`: Vite up on :5173, HTTP 200. Headless chromium renders the landing
             page (hero copy, Browse Places / Create Account CTAs). Screenshot recorded.
           - Console noise in devtools, none blocking: service-worker MIME error (dev-mode
             PWA, expected), geolocation denied (headless harness), and a console.error that
             VITE_ANALYTICS_API_URL is unset telling the reader to set it in .env.local,
             which mildly contradicts "you need no env file". Candidate follow-up: downgrade
             that to a dev-mode warn.
Found:     The browser tab title was "Lantern" + em dash + "Prototype" (apps/web/index.html),
           UI copy the no-em-dash rule covers, grandfathered in the baseline. Fixed to
           "Lantern (Prototype)" and locked the baseline reduction in the same commit.

11. Operator live onboarding run (the real test) โ€‹

  • Do: the operator walks the whole path herself in a fresh clone (kate-test-2), as the newcomer would. Agent mock runs (scenario 10) prepare for this; they do not replace it.
Result:    [x] in progress 2026-08-09; findings applied to the branch as they land
Findings:  1. `npm link` was buried as an optional aside -> promoted to a DAY_ONE step 1
              command (full record in the scenario 3 update above).
           2. `env:bootstrap` listed manual vars with no guidance -> per-var hints now print
              under each unset value (GH_PAT, GITHUB_OWNER, GOOGLE_APPLICATION_CREDENTIALS,
              OTP_TEST_PHONE, VITE_DEV_TEST_LOCATION), verified via dry-run, and
              ENVIRONMENT_SETUP gained "The per-developer values, and where each one comes
              from" as the long-form walkthrough.
           3. The OTP_TEST_PHONE default is a shared fictional number -> hint + docs now say
              to create your OWN Prelude test number (dashboard login comes from the
              operator). Test numbers only, per the standing Prelude guardrail.
           4. No mention of AI-assisted onboarding -> DAY_ONE section 4 reworked as "Onboard
              with the AI alongside" (auto-loaded project context, `/context-onboard` once,
              extension as the optional extra). The skill is `context-onboard`; there is no
              team-onboard skill.
           5. VS Code was not a prereq and nothing said to open the clone -> VS Code added to
              BEFORE_YOU_START and DAY_ONE prereqs (with the Windows-side + WSL-extension
              nuance) and `code .` added to both clone blocks with the accept-recommended-
              extensions note.

12. Code review round 2 (10-finder /code-review + adversarial verify) โ€‹

  • Do: full 10-angle review of the branch diff, verify every candidate against the code, fix everything verified.
Result:    [x] pass (all verified findings fixed; skips recorded with reasons)
Actual:    The big ones, all confirmed by execution before fixing:
           - guard-pr-ready.sh was committed 100644 (core.filemode=false swallowed the chmod),
             so on any fresh clone the guard was silently inert. Re-staged with
             `git update-index --chmod=+x`.
           - Three real bypasses in the hook (command-after-heredoc, decoy -h/--undo
             anywhere on the line, both verified live) and one over-block (any command
             merely MENTIONING the GraphQL mutation, hit twice during the review itself).
             Rewrote: per-segment --undo scoping, line-scoped heredoc strip, gh-api-anchored
             mutation check, fork-free fast path. 22 command-shape tests pass.
           - bootstrap-env.mjs skipped placeholder values on re-run ('your-key-here' literal
             vs real 'your-*-here' placeholders), which defeated both the #832 fix and
             today's hints. Fixed with the startsWith('your-') check the file already used
             elsewhere; verified with a placeholder .env.local in the mock clone.
           - doctor's 'lantern on PATH' now realpath-verifies the link targets THIS clone
             (it was green while linked to kate-test-3 from this checkout, live-verified)
             and warns with the other clone's path.
           - Notifications doc rewrite had dropped wave-accepted banners, which still exist
             (useAcceptanceBanners + ConnectionBannerStack); doc corrected.
           - Dedup pass: DAY_ONE section 0 now points at BEFORE_YOU_START instead of
             mirroring it; README teaches clone-only + defers to DAY_ONE; DEVELOPER_ONBOARDING
             defers its setup block to DAY_ONE; DIRECTORY_DEFINITIONS stops hand-listing
             every guide; ENVIRONMENT_SETUP's duplicate manual-vars section merged into its
             existing table (now five rows incl. OTP/location); .env.local.example's
             'or use ADC' contradiction fixed; OTP guidance moved to the shared
             dev-inputs.cjs entry so the VS Code Inputs panel shows it too.
           - Links broken by the folder move fixed (spec, worklog x2); live codespace plan's
             `npx lantern` instruction replaced; test-plan #442 cite corrected to #838;
             routing rule's ~15 min aligned to 10; WSL clone-inside-Linux-FS guidance added
             (BEFORE_YOU_START, DAY_ONE) for the /mnt/c symlink hazard.
Skipped:   with reasons, recorded in the PR body: merging the two Bash guard hooks
           (fast path removes the cost; merging risks the proven rule-17 guard),
           a shared hooks lib (cross-pointer comments instead), deriving SECRET_MAP
           from .env.local.example annotations (#832 class fix, noted there),
           deleting the root `npm run lantern` script (invocation-surface change,
           noted on #840).

Verified by reading (documentation changes) โ€‹

Every factual claim changed in this PR was checked against the source before editing, with file and line evidence: the ENVIRONMENT_SETUP step ordering, the DEVELOPER_ONBOARDING prerequisites and first-PR flow, the README structure section, the EBADENGINE cause (services/functions/firebase pins node 22), the missing merchant screens, the chats vs connections collection, and the lantern TTL. Two of #829's own claims turned out to be wrong and are corrected rather than propagated (see the PR body).

Explicitly out of scope โ€‹

  • ~500 remaining broken doc links: split to #833 with the two mechanical clusters named.
  • The Node 22 EBADENGINE bump itself: #838 (the old tracker #442 is closed), needs an isolated PR with a deploy dry-run.

Built with VitePress