Onboarding fixes + docs staleness (#829, #830) - Test Plan โ
| Field | Value |
|---|---|
| Branch / PR | docs/onboarding-fixes / PR #831 |
| Issues | Closes #829; #830 (publish command); split-out follow-up #833 |
| Env | Local worktree on the dev VM (Linux). No deploy surface. |
| Date | 2026-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 doctorfrom the repo root. - Expected: full check runs; no reliance on
npxor 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 -- doctorfrom 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 doctoron a machine with no link. - Expected: the row appears with the exact
npm linkhint, 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
./lanternform, 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/driveis 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-reviewon 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.