Persistent memory for AI agents working on docs.plus. Preserve these rules unless a maintainer explicitly changes them.
A task is usually governed by sections filed far from it, so reading in heading order misses rules. Find the task here and read every section listed. Sections not in this file carry their home in brackets — see §Filed by directory for the full map.
- Any task → §Memory And Rule Boundaries · §Package Manager · §Git And Commits · §Simplified English Mandate · §Workflow And Review Expectations
- Write or change a test → §Test Policy · §Test Orchestration And Authoring · §Workspace — playground harness · §Standalone Extension Development — round-trip specs [extensions] · §Hypermultimedia Extension — assert rendered DOM, not node attrs [extensions] · §Indent Extension [extensions] · §Webapp-Owned Hyperlink Popovers —
data-testidonly [extensions] · §Chat Media Attachments [chatroom] - Add or bump a dependency → §Package Manager · §Dependencies · §Shared Library Config · §ESLint Config · §Docker · §Test Orchestration And Authoring · §Learned Workspace Facts
- Build or change a publishable extension →
extensions/CLAUDE.md(whole file) · §Shared Library Config · §Extension Package Contract [release-extensions skill] · §Docker - Publish or cut a release →
RELEASE_POLICY.md(authoritative) · therelease-extensionsskill · §Git And Commits · §Skills And Prose - Editor / ProseMirror →
apps/webapp/src/components/TipTap/CLAUDE.md(whole file) · §TipTap Styling [webapp] - Webapp UI, styling, theme →
.cursor/docs/design-system.md(owns tokens and recipes) · §UI And Theme ·apps/webapp/CLAUDE.md(whole file — module layers, pad surfaces, mobile shells, overlays, motion, slug entry, landing) - Chatroom or messaging →
apps/webapp/src/components/chatroom/CLAUDE.md(whole file) · §Composer Link Dialog And Internal Links [extensions] - Document features →
apps/webapp/CLAUDE.md§Document Features (access, version history, TOC, comments, filters, panels) - Backend, hocuspocus, REST →
apps/hocuspocus.server/CLAUDE.md(whole file) · §Docker - Next product APIs, Validate, Status, Confirm, Health → §Next Product APIs [webapp] · §HTTP Modules [hocuspocus] · §Product HTTP · §Production And Docker Compose [hocuspocus]
- Admin dashboard →
apps/admin-dashboard/CLAUDE.md(whole file) · §Admin API And Dashboard [hocuspocus] - Supabase, SQL, RLS →
packages/supabase/CLAUDE.md· §Document Access [webapp] · §Anonymous Chat Read Path [chatroom] · §Admin API And Dashboard [hocuspocus] · §Learned Workspace Facts ·.cursor/rules/supabase.mdc - Docker, CI, deploy → §Docker · §Production And Docker Compose [hocuspocus] · §Workspace · §ESLint Config
- Write a standalone script → §Standalone Bun Scripts · §Workspace · §Package Manager
- Prose: README, CHANGELOG, report, JSDoc → §Skills And Prose · §Documentation And Comments · §Code Quality · §Extension Package Contract [release-extensions skill]
- Refactor or cleanup → §Code Quality · §Workflow And Review Expectations · §Webapp Module Layers [webapp] · §Test Policy · §Skills And Prose
AGENTS.mdstores durable docs.plus-specific invariants, maintainer preferences, and regression guardrails.- Keep large vendor or mechanical references in focused Cursor rules, not copied here:
.cursor/rules/design-system.mdc: design-system pointer + cardinal UI rules (source of truth:.cursor/docs/design-system.md; vendor daisyUI class reference is fetched from https://daisyui.com/llms.txt on demand — no longer vendored)..cursor/rules/react-floating-ui.mdc: React 19 ref and Floating UI interaction patterns..cursor/rules/supabase.mdc: SQL authoring style and focused Supabase file warnings..cursor/rules/tiptap.mdc: upstream Tiptap/ProseMirror reference workflow..cursor/rules/scripts-naming.mdc: scripts and Make-target naming convention; auto-attaches when editingpackage.json,Makefile, workflows, or files underscripts/.
- Long-form policy docs that an
.mdcrule points at live in.cursor/docs/. They aredesign-system.md,scripts-naming-convention.md,extension-version-cutover.md, andpanel-feed-seams.md.design-system.mdis the source of truth for the webapp visual language — tokens, themes, elevation species, state recipes, component catalog.scripts-naming-convention.mdis the timeless rule and the source of truth for scripts naming.extension-version-cutover.mdcarries rotating per-package cutover state; see §Extension Version Doctrine. One-shot migration docs may live alongside as siblings and are deleted with the cutover PR that completes them. - Durable agent memory lives at
.agents/memory/, a symlink to the Claude Code memory store, so Claude Code and Cursor read one copy..agents/memory/MEMORY.mdis the index. It is gitignored and stays that way — it records production hosts, project refs and infra details. Cursor reaches it through.cursor/rules/agent-rules-and-memory.mdc, which also carries the read order, because Cursor does not readCLAUDE.mdfiles at all. Any rule filed only in aCLAUDE.mdis invisible in Cursor unless that map names it. - The
Greptool does not follow that symlink, and it reports no matches rather than an error. A search of.agents/memorytherefore looks clean while the text is really there. Measured 2026-08-19:Grepreturned nothing where shellgrep -rl .agents/memory/returned two files. Never conclude fromGrepalone that a memory entry is missing, or that a stale fact is already gone. Name the store with a trailing slash —grep -rl <pattern> .agents/memory/. Searching from.agents/finds nothing under either-ror-R, because neither descends through the link, andfindneeds-Lfor the same reason.Readfollows the link on a full path. - Local working notes live in
Notes/(gitignored). Current reports, briefs, and the hocuspocus backlog sit inNotes/local-docs/. Start at that folder'sINDEX.md. Open it when the task is inject-content, Last left, occupancy, restore, REST gaps, scale, auth, composer, extension launch, TOC rail, or chat sash. The Grep tool skips this tree. Search withgrep -rl <pattern> Notes/local-docs/. Do not put these files indocs/. Security rows stay here and never become public issues. CONTEXT.mdat the repo root is the domain glossary (document-access terms and other named seams). Architecture reviews and deepenings use those names; do not invent parallel vocabulary when a glossary entry already exists.- Package-internal rules that don't generalize to the repo live in package-local
AGENTS.mdfiles next to the package. Today:extensions/extension-hyperlink/AGENTS.mdfor that extension's schema, commands, safety, click/preview, and clean-room harness. Cross-package rules (release flow, scripts naming, monorepo toolchain) stay in this root file. Read the package file in addition to the root file when working inside its package. - Directory-scoped rules live in per-directory
CLAUDE.mdfiles that Claude Code loads only when working under that path — see §Filed by directory. New rules go to the file that owns the directory, not back into this one. A rule that spans directories or names a prohibition stays here. Keep the §Filed by directory map current when a section moves. - How the per-directory files actually load (measured 2026-08-03, controlled probes):
- Reading any file under a directory injects that directory's
CLAUDE.mdand every ancestor's, in full, automatically. - Never
ReadaCLAUDE.mdwithlimitoroffset. A partial read suppresses the automatic full load for the rest of the session. You keep the few lines you asked for and silently lose the rest. Read it whole or not at all. - Bash file access (
head,grep,cat) does not trigger the load. A session that only shells out never gets the rules. - Root
CLAUDE.mdarrives at session start, not through the path trigger. - Untested: the
Greptool, andEditwith no priorRead. Assume neither triggers the load, and open the area file deliberately when planning.
- Reading any file under a directory injects that directory's
- When guidance overlaps, keep the project-specific policy in
AGENTS.mdor.cursor/docs/, and the detailed authoring/reference material in the relevant.mdcfile. - New rules land in the topical section that owns the subject.
## Learned User Preferencesand## Learned Workspace Factsat the end of this file are inboxes only. Append there when no section fits, one rule per bullet. File inbox entries into their owning section on the next tidy.
- Use Bun for package management, scripts, binaries, packing, and publishing:
bun install,bun add,bun add -d,bun run,bunx,bun publish,bun pm pack. - Never use npm, yarn, pnpm,
npx,npm publish, or package-lock/yarn-lock flows. - Keep
bun.lockas the only lockfile. Do not createpackage-lock.json,yarn.lock, orpnpm-lock.yaml. - The user sometimes prefers local command instructions over agent-run installs. Ask or provide the command when dependency changes are not clearly part of the requested task.
- Use root workspace commands:
bun run --filter @docs.plus/webapp devbun run --filter '*' build
- Required engines: Node >= 24.11.0 and Bun >= 1.4.0. Dockerfiles and CI runners must not drop below either floor.
- Do not commit unless the user explicitly asks.
- When authoring implementation or execution plans (e.g. superpowers
writing-plans, documents underNotes/superpowers/plans/), do not include commit messages,git commit,git add, or generic “commit the changes” steps. End with review and verification only; the developer inspects the diff and commits manually. .superpowers/andNotes/are gitignored (local agent scratch and maintainer notes). Do not force-add or commit either. Plans live inNotes/superpowers/plans/.docs/is tracked, public reader documentation. Never put a plan, a review, or a private note there. Mixing both roles insidedocs/behind!negations was built and rejected on 2026-08-19. One carelessgit add docs/then leaks megabytes of plans and audits.- Never write an unanchored bare filename in
.gitignore. A pattern with no leading slash matches at any depth.core.ignorecaseistrueon this macOS checkout, soQUICKSTART.mdalso swalloweddocs/api/quickstart.md. The file never appeared ingit statusand nothing reported an error. The four "Sensitive documentation" patterns are root-anchored for that reason. After any new ignore rule, prove the result withgit check-ignore -v <path>. - When executing multi-task plans from
Notes/superpowers/plans/, every task ends in a "Review checkpoint" step. Show the touched files and a short summary for review, then stop. Do not rungit add,git commit,git push,git stash, or amend. Quality gates (bun run … typecheck|test|build) still run between tasks. - Execute plans only in the current workspace (this git worktree). Do not switch execution to another worktree, path, or parallel checkout; all edits, shell commands, and tests must run against the open repo root.
- Work on
mainonly. Never create a new branch or worktree for any task, step, plan, or subagent. Do not rungit checkout -b,git switch -c,git worktree add, or passisolation: "worktree"to the Agent tool. No "feature branch", no "isolated worktree per task", no "let's branch off main for this fix". All edits happen on the currently checked-outmainworking tree. The user manages branching themselves outside the agent session. - This rule applies to every agent and subagent without exception. It covers every subagent dispatch: the
Agenttool,general-purpose,Explore,Plan, and specialized agents likecode-reviewer,frontend-developer,tdd-orchestrator, etc. The dispatching prompt must explicitly state "work on the currentmainworking tree; do not create branches or worktrees." Never setisolation: "worktree". Skills (superpowers:using-git-worktrees,superpowers:executing-plans,superpowers:subagent-driven-development, etc.) that suggest isolating per-task are overridden — stay onmain. Supervisor, orchestrator, and parallel-agent flows all share the samemainworking tree.
The release procedure lives in the release-extensions skill, which loads only when invoked. These prohibitions stay here because they bite in sessions that never invoke it — editing a workflow, tidying package.json, or adding a changelog tool.
- Never put
NPM_TOKENin CI for publishing. npm 2FA-on-write needs an OTP, so publishing happens on the maintainer laptop only. - Never run
git push --tags. The release script pushes an explicit tag list. - Do not reintroduce the removed root scripts
release,release:major,release:minor,release:patch,version*, or a parallelpublish. The only release entry point isrelease:family. - Do not auto-generate CHANGELOG entries from commit subjects. Lerna, Changesets and Release-Please are deliberately not adopted.
- Releases are stable-only: no
@nextdist-tag, no soak window, no promotion step.
- Keep production code DRY, KISS, YAGNI, SOLID, and industry-standard. Avoid overengineering.
- Export names must match file names. Fix typos in identifiers during refactors.
- Vocabulary — the word “chrome” is banned for UI everywhere: symbols, filenames, comments, and docs (2026-07-07 sweep removed every use). It is pre-Google GUI jargon for the trim around content; today it reads as the browser and states nothing about the job. Say what the thing is:
*Layoutfor responsive shells,*Barfor toolbars,*Surfacefor anchored panels. In prose use “shell” (layout wrapper), “frame” (border/shadow/background styling — the catalog-row label indesign-system.md), “surfaces” (docked regions), “controls” (media overlay/hover UI), or plain “UI”. “Chrome” may only ever mean the browser. ReserveEditor/EditorContentfor the TipTap/ProseMirror host only — not for a mobile/desktop layout switch. Chat composer canonical exports:ComposerLayout,ComposerDesktopLayout,ComposerMobileLayout, andMessageComposer.EditorContent(TipTap). Do not reintroduceComposerDesktopChrome,ComposerMobileChrome, orMessageComposer.Editorfor the layout picker. - Popover vs toolbar — two axes, never one word for both. "Popover" names the positioning container only (anchored, floating, light-dismiss — the
floating-popoverengine; shells are role-less by default because ARIA has no popover role). Content words name what's inside and carry the matching role. Usetoolbarfor action rows (the hypermultimedia media bar, hyperlink's preview). Usedialogfor floating forms (hyperlink create/edit). Compose when both axes apply (openToolbarPopover). Popover controllerkinds are consumer-namespaced open strings (e.g.media-…). Do not rename thepopovers.*/mediaToolbaroption surfaces onto a single shared word — they correctly name different species. Glossary:extensions/README.md§Vocabulary. - No
*Classes.ts/*Styles.ts/*ClassNames.tsmodules for Tailwind className constants. Extracting'pointer-events-none shrink-0 stroke-[1.75]'into a named export is over-engineering. Tailwind atoms are already the design tokens, and a second naming layer adds an indirection the reader has to chase without adding meaning. Inline className strings at the point of use (and accept the visual repetition across sibling components — it is locality of behavior, not a DRY violation). The same rule applies to local module-scopeconst FOO_BTN = '...'for single-file use. If two or more components genuinely share a non-trivial composite, lift to a shared component (<IconButton variant="composer-icon">) or a CVA-style helper — never to a bare-strings module. Pad and chat composer canonical: each toolbar/action button file owns its own className literal; the parent layout file (FormattingToolbar.tsx) owns the layout literals. Do not reintroduceformatToolbarClasses.ts,composerActionClasses.ts, or analogous files. - Feature folders use one central type module:
types.tsortypes/index.ts. Do not scatter feature-ownedtype/interfacedeclarations acrosshooks/,commands/,utils/,stores/, orcomponents/. - Keep feature layers separate and navigable:
index.ts->types.ts->hooks//commands//utils//stores//components/. - Treat performance and memory leaks as part of production readiness, not follow-up work. Audit Tiptap/ProseMirror changes for re-render storms, unsubscribed listeners, and detached views.
- Any
editor.on(...)orgetDefaultController().subscribe(...)call must return and call its unsubscribe on unmount unless it is intentionally module-scoped and guarded. - Keep debug/info loggers on editor core paths. Do not strip them during cleanup.
- Default: do not write new tests. Add a test only in these three cases. (a) The user explicitly asks for it. (b) The change pins down a regression that has actually shipped or been reported. (c) The failure mode is a real branching / ordering / race / parsing / projection bug that is hard to verify by hand. If you cannot name the specific failure mode in one sentence, do not write the test. "It seemed like a good idea to add coverage" is not a reason.
- Prefer integration over unit. When a test is warranted, default to Cypress E2E or real-stack integration that exercises actual user behavior end-to-end. Reach for a unit test only when the unit has branching logic dense enough that an E2E could not isolate a regression to it. Those units are parsers, projections, schema validators, pure utilities, message-grouping projections, and scroll-mode state machines.
- Never write these test shapes (delete on sight, do not generate): type assertions that re-prove what
tscalready proves; framework-behavior tests ("React renders", "Tiptap commands return truthy", "Supabase client exists", "useEffect runs"); mock-only flows where every dependency is faked and the assertion is "the mock was called with X"; snapshots of unstable output (full DOM trees, formatted JSON dumps, ProseMirror node JSON); "renders without crashing" smoke tests; trivial props-passthrough / getter / setter tests; coverage-chasing tests with no named behavior. - TDD is opt-in, not default. Follow strict TDD discipline only when the user explicitly asks for it ("write a test first", "let's TDD this", "/tdd"). This overrides any subagent skill that defaults to TDD on every fix or feature. Otherwise: design the change, ship it, verify with
bun run build/bun run checkand the relevant Cypress suite, and stop. - Observe tests pass before declaring done. Any test added in a change must be run locally and observed green — "should pass" is not evidence. Pre-existing tests that fail are fixed in the same change or explicitly
.skip'd with a one-line reason and an issue link; never silently disabled. - When unsure, ask. If you are mid-test and cannot articulate in one sentence which user-visible failure it prevents, stop and ask whether the test is wanted at all. The cost of a low-value test is review burden, maintenance, and false confidence. That cost is higher than the cost of a missing test we can add later when a real bug appears.
- Run
bun run buildafter major refactors before claiming completion. Never runnext build(or any production build) while a dev server is running against the same.nextdirectory. The productionBUILD_IDcorrupts the turbopack dev server intoInternal Server Error. With a live dev server, verify withtsc --noEmitplus the running app instead. git pushrunsbun run check:cithrough husky. That replica includeswebapp build:ci. The hook refuses the push if a Next development server is live, so the build does not write into a live.next. After a successful push, runrm -rf apps/webapp/.nextand restart the development server. To diagnose a corrupt development server, runls apps/webapp/.next: aBUILD_IDorprerender-manifest.jsonthere means a production build ran. Recover withrm -rf apps/webapp/.nextand a restart. Never reach forgit push --no-verify.- Validate full-document paste (
⌘A->⌘V) on editor changes that can affect paste or document transforms. - Authoring conventions and naming live under §Test Orchestration And Authoring.
- Do not call URL, router-adjacent, floating-menu, landing mobile-keyboard, bookmark/notification panel, or chatroom/slug skeleton UI production-ready or "ship" until the real interaction path is checked. Check that path in the browser, light and dark, on iOS Safari and Android Chrome.
- A hidden automation tab runs no
requestAnimationFrame. Chat realtime merges, Virtuoso, and toolbar re-renders wait on rAF, so a background tab shows a stuck UI that is not real. Checkdocument.visibilityStatebefore you trust a stuck UI, or use headless Playwright. For signed-in checks, usepackages/document-swarmactors. - On
/, verify hero+card vertical centering with the keyboard closed. Verify compact repositioning when the mobile keyboard opens (keyed to keyboard visibility, not slug-input focus). Verify expansion back when the keyboard dismisses even if focus stays on the slug input. history.pushStatecan leaveh/idin the URL bar where stripping only bookmark-style params no-ops — verify URL cleanup the same way. Context menus need pointer cursor, hover contrast, and full-width dividers verified in the browser too.- When driving these checks,
window._store(likewindow._editor) is exposed only on the editor playground route, not/or document routes. So toggle theme viadata-themeon<html>and apply the component's compact/active classes directly rather than poking the store. Confirm the Next webapp dev port first; it isn't always 3000 undermake dev-local, where 3000 may be a backend. - The mobile shell is user-agent gated on the server, so resizing the viewport never produces it.
getDeviceInforeads theuser-agentrequest header throughmobile-detectindocumentServerSideProps, and[...slugs].tsxpasses thatisMobileprop touseAddDeviceTypeHtmlClass. Browser checks of a mobile pad surface need a mobile user agent (CDPNetwork.setUserAgentOverride). A narrow window orcy.viewportalone still rendersm_desktop, and every mobile selector is then absent. The/editorplayground computes no device info in its own GSSP, so it renders nomobileLayoutRoot, no TOC drawer, and none of their controls. A mobile-viewport spec pointed at that route cannot pass under any user agent. - Reading
scrollTopright after writing it races the editor wrapper'sscroll-smooth. SetscrollBehavior = 'auto'first, or the assertion reads the pre-animation position and reports a failure that is not there.
- Cleanup/review skills (the
.cursor/skills/code-janitorpipeline and kin; itsSKILL.mddefines--reviewand the gated-approval mechanism) are autonomous by default. Do not gate every step; stop only when a decision is genuinely ambiguous. --reviewis opt-in. Default cleanup applies edits and prints one terse line per file.- Reports are terse next-step outlines, not detailed plans.
- Real senior-level refactors are in scope under the gated-approval mechanism: exported symbol renames, typo fixes, file moves, file splits, and dependency bumps.
- True safety rules remain absolute: do not change runtime behavior for handled inputs, edit generated files, or commit directly.
- All prose work routes through
tech-writer: README, CHANGELOG, reports, post-mortems, design docs, PR descriptions, and JSDoc/docstrings. Its Simplified English section is the house standard for sentences and vocabulary, and it lists the exempt surfaces. This file is exempt only for the text already in it; new and edited text follows the rules. - That standard is mandatory, not advisory: §Simplified English Mandate carries the obligation, the scope, and the subagent-dispatch clause.
- The rules in §Git And Commits apply to skills without exception. Skills never create branches or worktrees, and they operate in the current directory and branch.
- Simplified English is mandatory, not a preference. Every agent working here writes in it. The rules and the exempt surfaces live in
tech-writer. This section states the obligation and its scope; that section states the rules. No skill file other thantech-writer§Exempt may create an exemption, and a skill that states one is overridden. - Scope: every sentence an agent produces. Chat replies, task and review reports, plan text, code comments, JSDoc, commit bodies, and every file written to disk or to git. Commit subjects stay exempt. A maintainer changes this scope; an agent does not.
- Brevity an agent chooses never suspends it. A terse report style or a compressed status line changes how much you write, never the rules. Brevity the user invokes is different:
cavemanoverrides chat replies only, and never a file written to disk or to git. - This rule applies to every agent and subagent without exception. An output style reaches one system prompt, and a subagent runs its own. The dispatch prompt is therefore the carrier that does not depend on the subagent opening a file. The rule covers the
Agenttool,Task,Workflow,general-purpose,Explore,Plan, and specialized agents likecode-reviewer,frontend-developer, andtdd-orchestrator. Returned text is prose the parent reads, quotes, and pastes, so it is in scope. Supervisor, orchestrator, and parallel-agent flows carry the same sentence. Every dispatch prompt must explicitly state: "write in Simplified English; your returned text must comply; restate this sentence when you dispatch further agents."
- Comments and JSDoc explain non-obvious why, never narrate what. Names, types, and structure are the contract.
- Hard cap: ≤ 4 lines per JSDoc or block comment. If you need more, the code or the name is wrong — fix that instead. No section banners, no "Why X, not Y" preambles, no restating signatures or union members in prose.
- Cleanup includes deleting comments that violate this. "I didn't write it" is not a reason to keep them.
- When Task/subagent output is already visible in the UI, avoid repeating or summarizing it unless the user asks or multi-task synthesis requires it. An optional brief third-person completion line is fine — vary wording instead of identical confirmations every time.
- Do not carry plan or brainstorm labels into durable memory, commit messages, JSDoc, or inline comments. No "v1", "v2", "phase 1", "MVP", or "green doc" shorthand. Describe shipped behavior and intent in plain product/engineering terms; reserve version suffixes for real wire formats (API paths, schema types like
CommentAnchorV1, motion token names). - When the user asks for a review-first workflow (e.g. "wire a plan", approve-before-execute), draft the plan and pause for explicit approval before implementing. For UI/design tasks (toolbar redesigns, popover/URL-chip variants), the review artifact they expect before any code is a visual preview. That preview is a Cursor canvas or a standalone interactive HTML demo, one mock per variant/URL type, so they can approve the look first. Produce it as part of the plan rather than jumping to implementation.
- After substantive chatroom/webapp, landing/app-shell, Supabase, or extension-package work, expect a cohesive DRY/KISS pass before treating the work as production-ready. That pass is a dead-file import-graph sweep, redundant store fields, code-simplifier, and thermo-nuclear review when invoked. It is not optional follow-up polish. Landing/shell work also demands a performance pass (route-scoped JS/CSS, deferred auth, no duplicate viewport listeners) before ship.
- Keep
AGENTS.mdin full human-readable prose. Do not caveman-compress it or commit token-budget rewrites unless the user explicitly asks. Restore from backup if an agent compresses without approval. - For large HoE-style reviews (landing page, app shell, chatroom, TOC, sheets, composer, toolbar panels), build cohesive context from related modules first. Then wait for the explicit task before implementing. Avoid overengineering and overthinking in review output and refactor plans.
- When the user approves a numbered audit or wishlist backlog ("work on all of them"), implement the full list with a validation checkpoint per item. Full coverage does not justify extra abstraction layers. Keep each step minimal.
- "Do we pass CI?" asks for a per-gate verdict, not a summary. Run
bun run check:cifirst. Husky pre-push runs the same command.scripts/check-ci.tsreplicates the prod quality gates and prints pass, fail, or skip per gate. It names every gate it skipped, so a green run never implies a job it did not execute. Otherwise run the gates by hand — lint, Prettier, Stylelint,bun run typecheck, webapp Jest, the hocuspocus suite, andbuild:cifor webapp and admin-dashboard. Report each one as pass or fail. Name the gates you could not run instead of implying green. Backend E2E needs the Postgres and Redis service containers thatbackend-ci.ymlprovides. The clean-room extension suites are far cheaper than this file used to claim. Measured 2026-08-27 on Bun 1.4.0, with everydist/already built: 209 s for all five run one after another, and 89 s forbun run --parallel --filter '@docs.plus/extension-*' test. Per suite: hypermultimedia 90 s, hyperlink 61 s, placeholder 21 s, inline-code 20 s, indent 17 s. A wholebun run check:citook 4 m 50 s. Those numbers are the warm path only.scripts/run-tests.shdefaultsEXTENSION_DIST_READYto0, and at that default every package runs its ownpretestbuild first. The old "roughly 45 minutes" figure describes that cold path, and nobody has re-timed it. Quote the warm numbers only whendist/already exists.build:ciis a plainnext buildwith no dotenv layer, so passNEXT_PUBLIC_SUPABASE_URLandNEXT_PUBLIC_SUPABASE_ANON_KEYthrough the environment. The §Test Policy ban on a production build sharing a live dev server's.nextstill applies. - The repository is public, so a security finding never becomes a GitHub issue.
SECURITY.mdforbids a public issue for a vulnerability and routes reports tosecurity@docs.plus. An unfixed weakness in live production is a working recipe once published. On 2026-08-19 an audit backlog was split on this rule. Seventeen ordinary engineering rows became issues #153–#169. Every security row stayed in the local backlog file. The same test governs a commit, not only an issue. Do not commit a report that names an unfixed weakness with file and line references. Split any audit output the same way.
- Theme/UI color consistency is first-class. Every surface, including third-party pickers, must follow design tokens.
- The visual language source of truth is
.cursor/docs/design-system.md(tokens, themes, elevation species, shape scale, state recipes, per-component catalog). Thedesign-systemskill in.cursor/skills/is the working protocol. This file keeps the behavioral invariants; that doc owns the look. Update both in the same change when doctrine moves. - On DaisyUI-backed surfaces, prefer DaisyUI + Tailwind over bespoke nested hover/active stacks that fight parent controls. Vendor daisyUI class reference: https://daisyui.com/llms.txt (fetch on demand). Use
.cursor/rules/react-floating-ui.mdcfor generic React/Floating UI pitfalls. - daisyUI first, and never hand-roll a state daisyUI already owns. Reach for the component infrastructure first (
btn+ itsbtn-ghost/btn-square/btn-circlemodifiers,input,select,menu,tab,badge,collapse,skeleton). Then take the recipes indesign-system.md§State language, then bespoke CSS — in that order. Never override a daisyUI hover/active/focus state with an inlinecolor-mix()arbitrary value in a component. New colours or effects become:roottokens (+@themealias) and are consumed as token classes; that is §Change protocol and it is not optional. Measure before you deviate: mount a probe, hover it, readgetComputedStyle().backgroundColor. A guess that a default "won't read on this ground" is not evidence.btn-ghosthover, for instance, resolves to an opaquebase-300that is identical onbase-100andbase-200and tracks the theme. If a default genuinely fails, apply the doc's own escape hatch to the one instance that needs it. That hatch isbase-300for one more step on the well. Never apply it as a global override. - Themes are token sets (daisyUI blocks in
globals.scss): 3 base (docsplus/docsplus-dark/docsplus-dark-hc) + 2 opt-in premium light/dark pairs (docsplus-graphite[-dark],docsplus-paper[-dark]), added alongside — never replacing — the base. Dark-ness comes from the platform: daisyUI setscolor-schemeper theme, so shared dark tokens arelight-dark(<light>,<dark>)in:root/@themeand resolve automatically. Use NO theme-name lists, NO companion attribute, and NO Tailwinddark:variant (semantic tokens +light-dark()cover it).applyThemeToDom(stores/themeConfig.ts) writes onlydata-theme. Adding a theme (design-system.md §Themes → Adding a theme): daisyUI block (withcolor-scheme) +themeConfigunions/PREFERENCE_TO_THEME(light themes also joinLIGHT_RESOLVED_THEMES) + picker entry + per-theme[data-theme] { --pad-well/--color-media-* }. Add the per-theme block only where it differs from the shared default. Non-color light/dark differences (whole shadows) use a color-swap inside a shared structure, notlight-dark().Systemresolves to the base light/dark pair only, never a premium theme. - Floating overlays and modal scrims follow §Webapp UI Systems → Floating Surfaces And Modal Scrims (black-based scrims, unified panel frame, blur rules). Do not invent per-feature backdrop colors or
base-contentfull-screen washes. - Desktop pad workspace (TOC column, editor well, document sheet, docked chat) follows §Webapp UI Systems → Pad Workspace Surfaces. Use borders for docked regions. Use shadows only on floating overlays and dark-theme sheet lift.
- docs.plus is a Bun monorepo. Root
package.jsonworkspaces are"apps/*","extensions/*","packages/*". The deployableswebapp,hocuspocus.server, andadmin-dashboardlive underapps/. The five publishable@docs.plus/extension-*live underextensions/. Shared internals and tooling (floating-popover,floating-tooltip,eslint-config,release-tooling,playground,supabase,email-templates,document-swarm) live underpackages/. - Path-resolution invariants of that layout: every extension resolves
../../tsup.baseand../../tsconfig.base.jsonagainst the repo root.apps/webapp/next.config.jskeeps the repo root as its tracing root, and the per-package eslint shims import../../packages/eslint-config/*. @docs.plus/playgroundis a dev/test-only clean-room harness the extension Cypress suites consume as aworkspace:*devDependency. It ships adocs-playgroundbin and a browsersetupPlaygroundhelper. The bin generates the page shell + serves it, symlinking the consumer'smain.tsinto a temp dir so Bun's HTML bundler resolves it. The helper personalizes title/heading/tokens, wires the theme toggle, and returns#editor. Each extension'stest/playground/holds onlymain.ts(the editor fixture) and a 1-linetsconfig.jsonthat extends@docs.plus/playground/tsconfig.json;cypress/tsconfig.jsonextends@docs.plus/playground/cypress/tsconfig.jsonwith a localinclude. The package ships raw source (no build), is never published, and is never imported by anysrc/, so it stays out of everydist. Its manifest is COPY'd into each Dockerfile so--frozen-lockfileresolves the devDep.- Shared webapp utilities live in
apps/webapp/src/utils/;src/lib/was removed. Keep feature-local helpers colocated. Layer placement rules (utils vsui/vs layout shells vs feature folders) live under §Webapp UI Systems → Webapp Module Layers. Follow them before adding or promoting helpers. - When trimming playground-harness redundancy, limit to dead CLI, derived manifest fields, and small shell extractions (
extension-preflight.sh) — no shared Cypress factories or root screenshot wrappers.
- Root
package.jsonowns shared devtool versions: ESLint, TypeScript, Prettier, Stylelint, Jest,babel-jest,jest-environment-jsdom,@types/jest,@babel/preset-typescript, and related tooling. - Root
catalog:centralizes pins where used. Workspaces reference matching deps as"package": "catalog:". - Do not duplicate Jest/Babel dev dependencies in package workspaces unless there is an exceptional documented reason.
@tanstack/react-queryis root-cataloged at v5 for webapp and admin-dashboard. Use object syntax; mutation pending state isisPending, while queryisLoadingremains valid.- Toolchain split (deliberate): root
package.jsondevDependencies run TypeScript 6 and ESLint 10 by maintainer decision. Thecatalog:pins stay on TypeScript 5.x / ESLint 9.x pending a dedicated full-monorepo migration.tsconfig.base.jsoncarries"ignoreDeprecations": "5.0"— required for extension dts builds under TS 6; do not remove it. - The metascraper family STAYS PINNED to
5.50.6. Dropping it broke the production deploy on 2026-08-30, and the pin was restored. Rootoverridespinsmetascraper,@metascraper/helpersand the ninemetascraper-*plugins. The history below names the failure the pin prevents.@metascraper/helpers5.51.1(the latest at pin time) depends onmime@4(ESM-only) andrequire()s it. Bun (1.3.14 at pin time) throwsrequire() async module … unsupportedwhen a CJS modulerequire()s it. Becauseapps/hocuspocus.serverloads metascraper at REST startup (link-metadata module), this crashes the REST server. The crash cascades to the webapp slug page (SSRdocumentServerSideProps→fetchDocument→ REST down → redirect to/500).5.50.6is the newesthelpersstill on nestedmime@3(CJS) — the break landed in5.51.1(no5.51.0was published). There was no forward fix to "upgrade into" at pin time:helpers/mime/Bun were all already latest, and Bun'srequire(esm)fix (PR #30016) was canary-only. Hocuspocus's ownimport mime from 'mime'(v4 ESM) is unaffected — only metascraper's CJSrequireof mime breaks. Drop the overrides once metascraper ships a CJS-safehelpersor a stable Bun lands the require(esm) fix. After any metascraper bump, verifycd apps/hocuspocus.server && bun -e "await import('metascraper')"loads before shipping.- DO NOT DROP THIS PIN, and do not trust a local test that says you can. Attempted 2026-08-30 at
^5.56.2. Deploy run33365858244failed in Backend E2E with the exact documented error, on Bun 1.4.0, Linux x64:@metascraper/helpers/src/index.js:6 const mime = require('mime').default→TypeError: require() async module ".../node_modules/.bun/mime@4…". It was the only failing job, and it gated Build Verification and Deploy Production. Reverted bygit revertof the unpin commit. - Every local check said the drop was safe, and every one of them was wrong. These four all passed and none reproduces the failure — do not repeat them and conclude anything:
(a)
bun -e "await import('metascraper')"; (b)createRequire(...)('./src/index.js')on the helpers CJS entry; (c) a real REST start that bound:4000and servedGET /api/metadatawith a scraped title; (d)bun testat 578 pass. Even after the revert, loading the5.56.2CJS entry still succeeds on this machine, so the laptop cannot reproduce the fault at all. - Why the local checks lie.
bun -eevaluates in an ESM context where Bun can await the module graph. The real failure needs a CJS module on disk doing a synchronous barerequire()at module scope, reached through the graph CI builds. REST start is not enough either, becauselink-metadatareaches metascraper lazily;scripts/e2e-change-attribution.tsreaches the synchronous entry that REST boot does not. - The only authoritative signal is the Backend E2E job on a
(build):commit.bun run check:ciSKIPS that job locally. So a greencheck:ciis not evidence about this pin. If you ever revisit the drop, push it alone on a(build):commit and read that job — nothing on a laptop can settle it.
- DO NOT DROP THIS PIN, and do not trust a local test that says you can. Attempted 2026-08-30 at
- Bun 1.4.0 segfaults on a direct
require('re2'). Do not add one. Measured 2026-08-27: exit 139 onre2@1.24.1andre2@1.26.1alike. Bun 1.3.14 instead refuses the same file with a catchableNODE_MODULE_VERSIONerror, because the addon on disk is built for ABI 147 while 1.3.14 wants 137. So 1.3.14 looked safe only because it could not load the addon at all.re2arrives transitively under@metascraper/helpers, and the metascraper pin does not avoid it — the pinned and unpinned trees both carry a copy. Nothing here requires it directly, andurl-regex-safe, its real consumer, loads clean on both runtimes. The hazard is latent. Keep it that way. - A local
bun install --frozen-lockfiledoes NOT validate the lockfile. Only a cold install does. A warm~/.bun/install/cachemeans nothing is re-downloaded, so no tarball integrity hash is re-checked, and a corrupt lockfile passes on the maintainer's machine. Docker installs cold from the registry and verifies every tarball. Measured 2026-08-30: abun run updatesweep produced abun.lockthat passed locally, then failed the webapp Docker build with repeatedIntegrityCheckFailedonsass,sharp,eslint-plugin-react-hooksand@floating-ui/react. Reverting only the lockfile, with the same Dockerfile, built clean at exit 0 with zero integrity failures. After any dependency change, prove it with a cold install before trusting it — a Docker build is the cheapest one available.bun run check:cidoes not catch this, because it installs warm too. - Dependency update flow:
- Bump version ranges:
bun run update(patch + minor only; root catalog + everyapps/*,extensions/*,packages/*viascripts/update-packages.sh). Majors:bun run update --upgrade. Preview:bun run update --dry-run. The underlying tool isnpm-check-updates— notnpm-check, and not a verbosebun update --recursivewrapper; keepscripts/update-packages.shminimal. - After
update, runbun installat the repo root. Do not run parallelbun update/ installs inside individual packages; sharedbun.lockcan race withEEXIST. - When adding a workspace package, keep
bun.lockdiffs scoped to that package's graph. Avoid unrelated root devDependency churn from a blind fullbun install. Surgically merge lockfile entries if needed.
- Bump version ranges:
- Removed tools/scripts stay removed: per-package
update:packages,scripts/reinstall-packages.sh,reinstall:all-packages,update:all-packages.
- Unit + E2E stack: Jest and Cypress. Script names and
CYPRESS_PARALLELsemantics are defined in the naming convention doc; this section captures docs.plus-specific orchestration, Jest wiring, and authoring conventions. Policy (when to write a test, what shapes to avoid) is in §Test Policy. - Run order comes from
scripts/publishable-extensions.ts(the SSOT gate listscripts/run-tests.shreads at :162); each extension runs its ownbun run test:@docs.plus/extension-indent— Jest (localjest.config.cjs) then clean-room Cypress against builtdist/.@docs.plus/extension-hyperlink— clean-room Cypress against builtdist/(preceded bybun test srcunits).@docs.plus/extension-hypermultimedia— clean-room Cypress against builtdist/.@docs.plus/extension-inline-code— clean-room Cypress against builtdist/.@docs.plus/extension-placeholder— clean-room Cypress against builtdist/.@docs.plus/webappJest (jest --passWithNoTests, so an empty or temporarily absent suite does not fail CI/local runs). CI:stage.docs.plus.ymlandprod.docs.plus.ymlrunbun run --filter @docs.plus/webapp testin the quality-gate stage.
- Clean-room ports are unique per extension: hyperlink 5173, hypermultimedia 5174, indent 5175, inline-code 5176, placeholder 5177.
- Prod matrix-gates the extension suites by changed paths, so a green history does not prove a suite ran.
prod.docs.plus.ymlresolves the matrix throughdorny/paths-filteragainst.github/filters/extensions.yaml. That filter keys each extension on its own directory plus the shared inputs (package.json,bun.lock,tsconfig.base.json, the build-extensions action, and the extension build scripts). A suite nobody touches sits unrun for weeks. The hyperlink suite last ran on 2026-07-15, and it only showed a broken spec when a 2026-07-29 commit touched that directory. Confirm the job actually executed in the run you cite before calling a suite healthy. The unfiltered full-matrix run is prod's weekly cron (Sunday 00:00 UTC).scheduleforces all five throughFORCE_ALL, andbuild/deploystay excluded on that event, so the cron tests without deploying. Do not count onstage.docs.plus.yml. It triggers only on adevbranch that does not exist on origin, so it never runs except by manualworkflow_dispatch. - Jest wiring:
@docs.plus/webappkeepsnext/jestinjest.config.js.- Library packages that need Jest use a local
jest.config.cjs. Configureroots,testMatch,transform, andtestEnvironmentthere. - Prefer inline
babel-jestoptions injest.config.cjs; do not add per-packagebabel.config.cjsunless package-specific Babel behavior is required. - Add a library package test script as
"test": "jest --config jest.config.cjs". - Do not add package-local Jest stacks to
package.json; use the root dev dependencies. - Jest 30 uses the plural flag
--testPathPatterns, not the singular--testPathPatternfrom older docs/snippets. Correct it on sight. bun testis Bun's native runner — not a substitute for Jest where Next/Jest or local Jest configs are used.- Slice unit tests must call
enableMapSet()fromimmerat module scope. Slice files do not enable it themselves; onlystores/useStore.tsandstores/chat/useChatStore.tsdo at production load. Isolated slice instantiations otherwise fail with "MapSet plugin not loaded".
- Cypress conventions:
- Split tests by concern and include a README for scope.
- Use
it(), nottest(). Consolidate overlapping tests. - ProseMirror
handleDOMEvents.clickis not triggered by CypressrealClick()/.click(). Dispatch a nativeMouseEvent('click', { bubbles: true, clientX, clientY })usinggetBoundingClientRect()coordinates. Use the same pattern for popoverkeydownEscape dismissal. - ProseMirror input rules (
markInputRule/markPasteRule) do not fire oncy.type()— drive typing withcy.realType()(cypress-real-events) so the realbeforeinputpipeline runs. Keymap handlers (Tab / Shift-Tab, arrow-key mark exit) fire from a syntheticcy.get('#editor [contenteditable="true"]').trigger('keydown', { key, keyCode, shiftKey, bubbles: true })— same family as the click note above. ProgrammaticinsertContentdoes not trigger input rules; HTML parsing collapses leading whitespace, so build indented/space-led fixtures with real keypresses, notsetContent('<p> x</p>'). cy.realPress/cy.realTypeare PARENT commands — a chained subject is silently discarded.cypress-real-eventsregisters them with noprevSubject.cy.get('#editor .ProseMirror').realPress('Backspace')therefore resolves that element, throws it away, and dispatches a CDP key event at whatever the browser happens to focus. It never focuses anything itself. Any spec that sets the caret witheditor.commands.focus(pos)and then presses a key is racing real DOM focus. When focus has not landed, the key goes to<body>and the assertion fails with no clue why. Gate on focus first —cy.get('#editor .ProseMirror').should('have.focus')then a barecy.realPress(…)— which is a retrying assertion, not acy.wait. This was the 2026-08-08delete-collision.cy.tsone-in-three flake; sibling specs survived only because unrelated retrying assertions happened to buy the time.cy.wait('@alias')after navigation only matches future requests. Instantcy.interceptmocks often complete duringcy.visit()/cy.reload()load, so a post-navigationcy.wait('@alias')retries until timeout and reads as an infinite hang. Chatroom feed specs: register intercepts beforecy.visit, then gate on DOM (cy.waitForMessage,[data-msg-id], media expand/view controls visible) — notcy.reload()+ alias waits. Canonical:deep-link.cy.ts,open-unread.cy.ts,attachments.cy.tsvisitFeed(). Avoid conditional helpers that nestcy.wait('@storageSign')when the alias may never fire.- Scroll-tracking assertions must park the anchor mid-viewport before measuring. Cypress's default
scrollBehavior: 'top'leaves the target at y≈0, so scrolling the page by any real distance pushes it off-screen. Then floating-ui clamps the popover to itsshiftpadding and marks it hidden viahide({ strategy: 'referenceHidden' }). The assertion fails while the engine behaves correctly, which invites a pointless fix to the popover engine —autoUpdate({ animationFrame: true })was tried and reverted. Pin the anchor to a known viewport offset first, and assert equalΔtopbetween popover and anchor rather than against the scroll distance. Canonical:extensions/extension-hyperlink/cypress/e2e/scroll-stickiness.cy.ts. - Gallery/lightbox Cypress:
@storageSignmocks should return bothsignedURLandsignedUrl. Gallery<img>tags need loadable URLs (a 1pxdata:URL is fine). Feed tiles may paint via CSS background, so bogusexample.testpaths can pass feed assertions yet fail once the lightbox mounts real<img>elements.
- Naming:
- Cypress E2E directories and files use kebab-case (
copy-paste/,keyboard-shortcuts/,clipboard-validation.cy.js); noe2e-or numeric ordering prefixes unless the reason is documented. - Unit test files use camelCase and match the source module:
<moduleName>.test.ts; performance tests use<moduleName>.performance.test.ts. - Cypress support modules use camelCase; fixture files use kebab-case; fixture directories may use camelCase when mirroring a command name.
- Avoid sprint, phase, audit, or ticket names. Test descriptions describe behavior, not ticket IDs — within a file, use either
should ...phrasing or bare verbs consistently.
- Cypress E2E directories and files use kebab-case (
packages/eslint-configis ESM with"type": "module".- Three-layer config:
index.js: base TypeScript + Prettier +simple-import-sort; no React.next.js: base + React + hooks; used by webapp/admin-dashboard.library.js: base +explicit-module-boundary-types+no-console: warn; used byextension-*packages.
- Consumers use 2-line ESM imports. Do not add
createRequirebridges. - React plugins load only in
next.js, never in library or backend configs. packages/eslint-configuses onlyeslint-config-prettier; its flat config does not loadeslint-plugin-prettier.- Flat-config shim naming: root / webapp / admin-dashboard use
eslint.config.mjs(packages without"type": "module").hocuspocus.serverandextension-*useeslint.config.js(they declare"type": "module"). Seepackages/eslint-config/README.md. - Lint gate: root
bun run lintiseslint . --max-warnings=0.bun run checkincludes it plus format. Husky pre-push runsbun run check:ci. Pre-commitlint-stagedformats staged files and enforces zero ESLint warnings on those files. - Pre-push:
scripts/hooks/pre-push.shrunsbun run check:ci. It refuses the push if a Next development server is live.check:pushstays the fast local lint + styles + typecheck gate.
- Keep root-level shared config as the single source of truth.
tsconfig.base.jsonapplies to allextension-*packages. Packagetsconfig.jsonfiles only declare localoutDir,rootDir,include, andexclude.- Package
excludemust list colocated test paths:src/**/__tests__/**,src/**/*.test.ts. Otherwise tsc can expand the computed source root abovesrcand tripTS6059. tsup.base.tsexportsdefineTiptapExtensionConfig(overrides?).- The factory is intentionally Tiptap-specific. It hardcodes
@tiptap/coreand@tiptap/pmexternals. - Build shape: ESM + CJS, dts, production sourcemaps/minify, and
esbuildOptions.pure = ['console.log', 'console.debug']in production. - Do not use
drop: ['console']; it stripsconsole.warnandconsole.error. - A package's
tsup.config.tsshould calldefineTiptapExtensionConfig()throughdefineConfig(...); pass overrides only for package-specific behavior. - Overrides are shallow. Function-valued options such as
esbuildOptions,external, anddtsreplace the base value. If a caller overridesesbuildOptions, it must preserve the base pure policy manually.
- The factory is intentionally Tiptap-specific. It hardcodes
extension-hypermultimediaintentionally preservesconsole.errorfrom itsLoggerwrapper (src/utils/logger.ts, error-only by design — do not reintroducewarn/debug) under the shared tsup factory. Note logger changes in its CHANGELOG.@docs.plus/floating-popoverand@docs.plus/floating-tooltipare bundled into each consuming extension'sdist, never externalized. Both areprivateworkspace packages (never published), wired asdevDependenciesworkspace:*inextension-hyperlinkandextension-hypermultimedia. Theirtsup.config.tsexternalmust stay['@tiptap/core', '@tiptap/pm']. Do not add a'@docs.plus/floating-*'entry, or tsup emits a bare import into the published bundle that throwsMODULE_NOT_FOUNDfor external npm consumers.@floating-ui/domstays external (real published dep). Verify after any tsup change: builtdist/index.{js,cjs}must contain no@docs.plus/floating-*import/require. Neither package ships CSS. Consumers carry lockstep skins with one split rule..floating-popover*shell blocks use each package's own tokens, but the.floating-tooltipblock is identicallight-dark()literals by contract. Both bundles style that one global class, and per-package tokens would let cascade order pick one bundle's look for every bubble.scripts/extension-preflight.shenforces the byte parity.- Root
LICENSEis the single committed license.- Each publishable package adds
/LICENSEto package.gitignore. prepackcopies the rootLICENSEbeforebun publishorbun pm pack.- Symlinks fail because Bun pack drops them. Hard links fail because git stores independent copies.
- Each publishable package adds
- Shared release scaffolding lives in
@docs.plus/release-tooling— an internal workspace package exposingrelease-prepackandrelease-preflightasbincommands. The scripts are data-driven. They derive the consumer's package name and dist-artifact list from its ownpackage.json(name+exportsmap), so there is no per-consumer parameterization. Same DRY principle as@docs.plus/eslint-config,tsconfig.base.json, andtsup.base.ts— cross-package scaffolding is hoisted, never copied. Publishable libraries wireprepack/prepublishOnlyto these bins per the Type 4 contract in the naming convention doc. - Do not centralize package-specific files:
README.md,CHANGELOG.md, package source, 3-lineeslint.config.mjs/eslint.config.jsshims, orpackage.jsonfields.
- Use one Docker base tag everywhere:
oven/bun:1-slim. - Do not mix
1-alpine,1-slim, or hardcoded patch tags. - VERIFIED IN PRODUCTION, deploy
33367037524on 2026-08-31. All jobs green, 9 of 9 containers healthy. Build Docker Images fell from 997.4 s cold to 683 s. Thechown -Rstep has 0 occurrences in the log, and its replacementCOPY --from=base --chown=costs 14.0 s for the whole tree. The webapp image fell 727 MB to 332 MB, a true side-by-side because the previous deploy's images were still on the host; the untouched admin image stayed at 186 MB, which is the control. The backend image is 961 MB on amd64. The front-half cache fixes did not pay on that deploy and were not expected to — only 2 of 165 steps wereCACHED, because it was the first build with the new layer structure. They pay from the next(build):deploy: both webapp installs should printCACHED, andnext buildshould fall below 277 s. - Never
chown -Ra workspace tree in a Dockerfile. Stamp ownership withCOPY --chowninstead. A recursive chown rewrites every inode, so Docker stores a second full copy of the tree. Measured 2026-08-28 onapps/hocuspocus.server/docker/Dockerfile.bun: oneRUN chown -R appuser:appuser /applayer held 1.91 GB, and removing it took the backend image from 5.67 GB to 3.24 GB. Only the runtime write path needs stamping —storage.local.ts:16resolves./temp/<plugin>againstprocess.cwd(), which the entrypoint sets to/app/apps/hocuspocus.server. Leavingnode_modulesand the generated Prisma client root-owned is a security gain, not a cost. - A production install in a service image must be scoped with
--filter. A bare rootbun installresolves every workspace member, because--frozen-lockfileneeds every manifest COPY'd. The backend image therefore shippednext@15.5.21,next@16.2.12, four@next/swcnative binaries,react-icons,@emoji-mart/dataandtypescript— about 950 MB the REST, WS and worker processes never import.--filter '@docs.plus/hocuspocus'took the image from 3.24 GB to 1.42 GB and the--productioninstall from 54.9 s to 17.1 s. Keep the other apps' manifests COPY'd; the filter scopes the install, not the resolution. - Docker build timings do not transfer between architectures. Judge a build change from the production log, never from a laptop. Measured 2026-08-28: the same
chown -Rcost 1.8 s on arm64 macOS with OrbStack and 125.3 s on the amd64 host — about 70x. Sizes and per-step shares do transfer; absolute seconds do not. Profile with--no-cache --progress=plainand report share of total. - Compose does NOT duplicate a build when two services share one build definition.
rest-apiandhocuspocus-serverdeclare the same context, dockerfile and target. Measured:docker compose build --no-cache rest-api hocuspocus-serverranbun install --frozen-lockfilethree times, which is one pass through the Dockerfile's three installs, not six. BuildKit shares the layers and only stamps a per-service label, so the two image IDs differ while the content is identical. Do not "fix" this with a build-once-then-docker tagstep, and correct any comment claiming it duplicates context transfer. - A volatile
ENVabove an install invalidates it on every build. Declare per-deploy values below the install. AnENVenters a laterRUN's cache key; an unchangedCOPYdoes not. Measured in warm production deploy32946907649: no webapp source changed since the previous deploy, yet the builderbun installstill ran 158.8 s while the same install in two sibling stages wasCACHED. Fixed 2026-08-28 by movingGIT_HASHandNEXT_PUBLIC_GIT_HASHbelow the install inapps/webapp/docker/Dockerfile.bun, keeping them abovebunx next build, which still needs them because Next inlinesNEXT_PUBLIC_GIT_HASHinto the client bundle. Verified by a cache-hit assertion changing onlyGIT_HASHbetween two builds: 299 s with 7 cached steps became 62 s with 58 cached steps, the installCACHEDandnext buildcorrectly still running.BUILD_IDstays above, becausedocker-compose.prod.ymlnever passes it, so it is a constant empty string.- The admin-dashboard half of that edit is NOT safe as written.
apps/admin-dashboard/docker/Dockerfile.bun:67is the LAST entry of a multi-lineENVand carries no trailing backslash. Deleting it alone leaves line 66 ending in\before a blank line, the documentedNoEmptyContinuationshape, and theENVthen swallows the nextCOPY. Rewrite line 66 in the same edit, or leave admin alone.
- The admin-dashboard half of that edit is NOT safe as written.
- A BuildKit cache mount survives the deploy's prune, but only while it was used inside the window. Measured 2026-08-28 against a real
exec.cachemountrecord. The deploy's own command,docker builder prune -af --filter "until=24h", reclaimed 0 B and left the record intact, and the next build hit the mount. The control,until=1s, reclaimed 28.55 GB, removed the record, and the next build missed. Sountilfilters on last-used time and cache mounts are in scope. Real deploy gaps are median 2.2 h with 1 of 9 over 24 h, so a mount would be warm on roughly eight deploys in nine. A mount also survives a changingARGwith the layer cache on, which is the webapp case, but comes back empty under--no-cache, so the backend build would gain nothing from one. - A runtime stage must not descend from a build stage that installs a toolchain. The webapp
runnerusedFROM base, andbaseinstallspython3,make,g++andnodejsfor the build stages, so the shipped image carried a compiler it never used. It now usesoven/bun:1-slimdirectly, which still satisfies every command that stage runs —groupadd,useradd,rm,findandbun— and already shipsca-certificates.apps/admin-dashboardran the same Next standalone shape on the plain tag all along. Verified 2026-08-30: zerog++layers indocker history, and nog++orpython3binary in the image.noderemains, becauseoven/bun:1-slimships it. - An install whose
node_modulesnothing copies out is pure waste — delete the install, not the stage. The webappdepsstage ran a fullbun install, butbuild-extensionsandbuildereach copy only manifests and source, then install for themselves.depsis still needed as the shared manifest-layout stage, so removing the whole stage would be wrong. Four lines deleted, not a stage. --parallelis not adocker compose buildflag in Compose v2. It is absent fromdocker compose build --help. Named services already build concurrently. Do not add it back.- A cache-ordering fix cannot be measured with
--no-cache. A no-cache build has no cache to hit, so it reports zero for a correct change and invites reverting it. Verify those with a binary cache-hit assertion — two builds with the cache on, changing only the intended input, then check the step printsCACHED. That assertion is architecture-independent, so it runs anywhere. - Do not copy
node_modulesbetween Docker stages. Bun uses symlinks into a virtual store; copied installs can break module resolution. - Any stage that runs
next build, extension builds, or config that requires deps must runbun install --frozen-lockfile. - Copy only
package.json,bun.lock, and the workspace tree between stages. - Any Dockerfile stage that runs
bun run buildfor@docs.plus/extension-*must alsoCOPYthe root-level shared configstsconfig.base.jsonandtsup.base.tsinto the build context. Each extension'stsconfig.jsonextends../../tsconfig.base.jsonand eachtsup.config.tsimportsfrom '../../tsup.base'; missing either file fails the extension build withCould not resolve "../../tsup.base". Affected Dockerfiles:apps/hocuspocus.server/docker/Dockerfile.bunandapps/webapp/docker/Dockerfile.bun(build-extensionsstage must copy them via--from=deps). - A stage that rebuilds the workspace from a prior stage must copy all three roots, not just
packages/. Theapps/* + extensions/* + packages/*layout splits the workspace, so anyCOPY --from=<stage> /app/packages ./packagesmust be followed byCOPY --from=<stage> /app/apps ./appsandCOPY --from=<stage> /app/extensions ./extensionsbeforebun install --frozen-lockfile. Without theapps/*andextensions/*members the frozen install fails withlockfile had changes, but lockfile is frozen. Stages that wire this: webappbuild-extensions+builder, admin-dashboardbuilder, hocuspocus.serverproduction. Abuilderthat imports the extensions needs the builtextensions/(withdist/) from the build stage; manifests alone are not enough. - Build
@docs.plus/floating-popoverand@docs.plus/floating-tooltipbefore the extensions that bundle them — inside Docker too. The hyperlink and hypermultimedia extensions bundle both (not external; see §Shared Library Config). Any stage that runs theirbun run buildmust firstCOPYand build bothpackages/floating-*packages. Wired in webappbuild-extensionsand hocuspocus.serverbase;scripts/build-extensions.shand the.github/actions/build-extensionsdist-cache key cover both. The admin-dashboard builds no extensions, but it still copies both packages'package.json. Without that copy, the extensions'workspace:*references fail to resolve under--frozen-lockfile.
- Root
tsconfig.jsondoes not includescripts/. IDE lint errors such as missingnode:fsorimport.meta.dirinscripts/*.tsare usually noise. - Smoke-check standalone Bun scripts with:
bun build scripts/<file>.ts --target=bun --outfile=/tmp/out.js- Avoid ad-hoc
bunx tsc --noEmit <file>unless passing--ignoreConfig; otherwiseTS5112can fire whentsconfig.jsonexists.
These sections moved verbatim into files that load only when you work in that directory. Section names are unchanged, so every section reference in this repo resolves here. Router entries tag a section's home in brackets.
apps/webapp/CLAUDE.md · router tag [webapp]
§Webapp UI Systems · §Webapp Module Layers · §TipTap Styling · §Pad Workspace Surfaces (desktop) · §Mobile Bottom Sheets And Overlays · §Mobile Chat Pane · §Mobile Document Pad · §Floating Surfaces And Modal Scrims · §Motion System (motion v1) · §Slug Page Entry And Skeletons · §Collab Provider Status · §Landing Page Shell And PWA · §Next Product APIs · §Document Features · §Document Access · §Document Version History · §TOC And Heading Actions · §Heading Fold Crinkle · §Document Comments · §Document Filters · §Bookmark And Notification Panels
apps/webapp/src/components/TipTap/CLAUDE.md · router tag [TipTap]
§Editor Architecture · §Document Model And Migrations · §Heading Schema · §HeadingScale · §Editor Performance · §Editor State And References
apps/webapp/src/components/chatroom/CLAUDE.md · router tag [chatroom]
§Chatroom And Messaging · §Optimistic Message Lifecycle · §Message Grouping Projection · §Chatroom List (Virtuoso) · §Chatroom Realtime · §MessageComposer · §Chat Media Attachments · §Mention Picker · §Anonymous Chat Read Path · §Chatroom Staged Skeleton
apps/hocuspocus.server/CLAUDE.md · router tag [hocuspocus]
§Persistence · §Retention and schema · §Runtime limits · §Documented hazards we accept · §Backend And Infrastructure · §HTTP Modules · §Hocuspocus Server · §Admin API And Dashboard · §Production And Docker Compose
apps/admin-dashboard/CLAUDE.md · router tag [admin]
The client half of §Admin API And Dashboard. Not a moved section — it exists because the hocuspocus file does not load under apps/admin-dashboard/.
packages/supabase/CLAUDE.md · router tag [supabase]
§Supabase
extensions/CLAUDE.md · router tag [extensions]
§Extension Workflow · §Standalone Extension Development · §Hyperlink Extension · §Webapp-Owned Hyperlink Popovers · §Composer Link Dialog And Internal Links · §Hypermultimedia Extension · §Indent Extension
.cursor/skills/release-extensions/SKILL.md — invoke the skill · router tag [release-extensions skill]
§Publishing And Releases · §Extension Package Contract · §Release And Publish · §Extension Version Doctrine
Inbox only. New cross-cutting working-style preferences go into §Workflow And Review Expectations (or the topical section that owns the subject). Append here only when no section fits, one rule per bullet. File entries into their owning section on the next tidy.
- After substantive pad/TOC/design-system/chatroom work, run the continual-learning memory updater so regressions are not repeated.
- Settings Documents list UX should follow a Google Docs-like per-row ⋮ menu, not inline visibility toggles. The menu lists Open, Copy link when public, Rename, Duplicate, Favorite or Unfavorite, then Private and Read-only. Favorite sits after Duplicate and before the Private divider. No toolbar star. Name is Favorite, never Bookmark or Pin (see CONTEXT.md §Documents list). Keep it minimal and size-fit for the settings panel. On mobile the Documents toolbar must stay 44px-target size-fit (flex/truncate sort, icon-only Trash, no desktop-width overflow). Private ON must clear and disable Read-only on that menu and on
DocumentSettingsPanel(see §Document Access). - Date sorts on Settings → Documents use Date buckets on the rest after Favorites. No Favorites heading.
- Last opened is a sort. The pad owner stamps
lastOpenedAtviaPOST /api/documents/:documentId/opened. Last modified does not move. - Owner live list and Owner Trash list lead with
LuFileText, not first-page paper. Grid is 4/3 first-page paper (DocumentPreviewPaper). No screenshot thumbs. Create stays empty-state only. - TOC long heading titles must wrap (e.g.
text-pretty/whitespace-pre-line), never truncate — use the full row width left of the trailing chat/unread rail. Tree connectors use daisyUI nested menu rails ontoc__childrenonly (vertical rails, no L-elbows). Accept stock rail opacity — do not reintroduce a TOC::beforestrengthen overwrite. - Mobile heading control stays a one-frame stepper, name first:
[name | − | +]. Do not replace it with a chip that opens a list. The three cells share one join; no inner box around plus and minus. When the caret is Title, the join uses the sameis-activecolor as other heading levels. - TOC chat/unread/presence all live in-flow in
TocRowTrail. Desktop presence is<AvatarStack anchor="right">right of the chat trigger (right-anchored, fixed right edge, grows left,translate-x-3to the column inner edge). The scrollbar must stay inside the column, so do NOT widen.toc__scrollto overhang presence beyond the wrapper. That widening pushed the scrollbar out over the editor. Fix broken shipped UI motion (e.g.RollingNumber) in place — never strip it for a static fallback unless the maintainer asks. - Desktop TOC tick rail shipped 2026-09-18. Durable rules live in webapp §TOC And Heading Actions and §Pad Workspace Surfaces. Look lives in
.cursor/docs/design-system.md§TocTickRail. Names live inCONTEXT.md§Pad outline. Settled don'ts live in rootCLAUDE.md. Persist last committed wide width only (docsy:toc-width, greater than 240). Stored 240 or less is missing and opens at 320. A short tick stack sits in the middle of the live rail. Chat open and close tween that offset with--motion-panel. Sash drag has no tween. Do not persist 32 or 240. Do not lock the stack to the window mid-line. On a long rail, spy follow is keep-in-view and depth-fit after the fold filter. Do not pin the spy tick to the rail top or bottom (nearest/ edge-stick). Do not steal the rail while the pointer is on the minimap. - Docked chat sash-close uses the same snap recipe as TOC: linear paint below min, fade the inner column, snap at half min, abort back to min, settle with
--motion-overlay-in120ms. Do not put a transform on the chat panel. The Close button stays. Do not persist a below-min height. Wide TOC stays full height when chat is open (L-shape). Pad seams stay--pad-divider/base-300. Do not restyle docked borders in a session. - Chat feed image tiles are album-style: single scales from intrinsic size. Multi-image uses absolute rects (2–4 pack + complex pack for n≥5 / wide ratios) with cover-crop inside cells, not a fixed grid and not a separate tall-stack mode. Show all visuals up to the attach cap (no feed
+Noverlay). - Do not use Telegram/Discord (or other peer-product) names in code symbols, filenames, or identifiers. Use domain vocabulary (feed album, visual layout, mosaic pack). Peer names may appear only in prose when citing UX parity.
- Chat message feed must stay vertically scrollable only — media cards must fit the column and never introduce horizontal overflow (especially on mobile).
- Local-dev public DX is one command (
make dev-local): bun install (if needed), DB migrate, and extensiondistbootstrap stay inside that flow and must fail loud. Do not advertise internal preflight helpers as Makefile help / README targets, and keep the README local-dev path minimal. - Chat media gallery zoom/pan/keyboard and mobile ⋯ overflow changes are not done until verified in the browser. Verify desktop click/wheel/keyboard; iOS pinch/double-tap/pan; and mobile overflow opening a body-portaled bottom sheet, never nested under the pill’s
backdrop-blur. Do not claim fixed from code inspection or DevTools assumptions alone. - Hypermultimedia media-toolbar ⋯ overflow must toggle on repeated click. It must sit flush under the toolbar’s right edge (
bottom-endon the bar, tight top gap), not a large offset under the ⋯ alone.@docs.plus/floating-popoveroutside-dismiss must ignore the toggle trigger or the menu race-reopens and never appears to toggle. - Chatroom feed error / empty-load states should stay Discord-like and minimal. Use calm plain-language copy (no raw
403/permission deniedstrings), design-system tokens, and the same composition on mobile sheet + desktop docked panel. Research and compare before shipping a louder custom layout. - After a local
check:cior husky pre-pushbuild:ci, do not ask the maintainer to runrm -rfon a.nexttree.scripts/check-ci.tsandmake dest-localalready drop a leftover production.next. - Mobile bottom sheets must use the library grabber. Empty
Sheet.Headermountsreact-modal-sheet's DragIndicator. Do not add a house grabber. - Path A is the product shell: deepen the installed PWA (same site, Home Screen / dock). Do not start Electron, Tauri, store-listed native apps, or a native rewrite. Last opened on Home is for every signed-in owner, not only an installed window. Do not gate that list on
isPWAInstalled. - Copy success uses daisyUI
swap/swap-active(copy fades to a check). Do not remount the control or playdoc-region-in. Do not usetransition-all. Do not add aCopyFadewrapper.CopyButtonis the standalone control. Menus and sheets keep their own host button and putaria-hiddenon the swap root. A menu that closes after copy usesuseCloseAfterHoldandCOPY_FADE_HOLD_MS(MOTION_PANEL_MS × 2inapps/webapp/src/utils/motion.ts), and only after a successful copy. Documents ⋮ must cancel that hold on Favorite, Private, and Read-only. The notification bell is stacked opacity for three states, notswap; disable it while an update is in flight. TOC Copy link, gallery copy, and chat file-card copy stay hard cuts until asked. - Hide Report on the chat message card menu. Keep the Report row in
useMessageActionMenuItems.tsxatdisplay: falseso the desktop context menu and the mobile long-press menu both omit it. Document Report in Settings stays. Do not restore the chat Report row. Do not tell/termsreaders to open the chat menu and choose Report. - Chat message action titles stay shared for the right-click menu and the hover ⋯ menu: Copy Link, Copy to Doc, Edit, Delete, in
messageActionMenu.ts. Edit sits above Delete in both. Do not rename Copy Link to Share message link when the message has files. Do not put Message on Edit or Delete. Do not let the two menus drift apart. - Own empty profile card is one row: "No bio or links yet." on the left and "Add bio and links" on the right. That button closes the card and opens Settings on the Profile tab. Someone else's empty card stays the plain line only. Do not restore a filled primary button, a centered column, or a pencil icon on that empty state.
- Change digest email body must read like the pad. Keep the mail header, greeting, document title, Open, and footer. A line sits under the changed-since line. Each changed heading is bold and links to that place on the pad. The passage sits under the heading. Added text is green. Removed text is red and struck through. A new chat sits under its heading. A chat in a channel that is not a heading stays in its card. Do not put Chat or View on the heading row. Quiet headings stay out of the mail. The default sends one mail per document. Admin Notifications can switch to one combined mail. The same card sets the HTML size limit in KB. The default is 90. The range is 10 to 102. A missing Redis value stays on one mail per document and 90 KB.
Inbox only. New workspace facts go into the topical section that owns the subject. Append here only when no section fits, one rule per bullet. File entries into their owning section on the next tidy.
- Local working notes (09-17). Reports, briefs, and the hocuspocus backlog live in
Notes/local-docs/. Start at that folder'sINDEX.md. Route by inject-content, Last left, occupancy, restore, REST gaps, scale, auth, composer, extension launch, TOC rail, or chat sash.gio.mdis a copied Awesome Supabase list inNotes/scratch/. - docs.plus is a claimed, verified Context7 library at https://context7.com/docs-plus/docs.plus. Context7 feeds Cursor, Claude Code and Windsurf over MCP, so it is what an AI assistant reads about this project. It does not re-crawl on push. After a major documentation change — a README rewrite, a changed extension API, a release CHANGELOG entry, new pages under
docs/, or an edit tocontext7.json— open the library page and press Refresh, or every agent keeps serving the previous snapshot. Parsing config lives incontext7.jsonat the repo root, never the dashboard, so it stays reviewable in a diff. The authoritative field list ishttps://context7.com/schema/context7.json, which isadditionalProperties: falseand capsdescriptionat 200 characters and eachrulesitem at 255; the claim error blames "unknown fields" when the real failure is a length limit. Ranking cannot be tuned: library search is name-based anddescriptionis only a fallback the model overrides. - pg_cron email jobs (
compile_digest_emails,process_email_queue, digest cleanup) run on Supabase cloud (tglymsfloxmouzjuoycu), not the app'sDATABASE_URLDigitalOcean Postgres. Grafana cron-stale alerts read worker metrics fed by Supabase RPCget_cron_job_health. - Email notification path: pg_cron SQL →
pgmq.send('email_notifications_queue')→ hocuspocus-workerconsume_email_queueRPC → BullMQ → SMTP/Resend/SendGrid (pg_cron never calls HTTP). Prodpublic.email_queuecheck (email_queue_status_check) can drift from repo and omitprocessing; align withpackages/supabase/scripts/07-5-email-notifications-pgmq.sql. - The hocuspocus/Prisma DB (
DocumentMetadata/Documents; schemaapps/hocuspocus.server/prisma/schema.prisma) runs locally as docker containerdocsy-postgres-local(host:5432, dbdocsplus) — separate from Supabase local (supabase_db_docsplus_supabase,:54322).DATABASE_URLis in root.env.local; no hostpsql, so query viadocker exec -i docsy-postgres-local psql …and strip the?connection_limit=…&pool_timeout=…params (libpq rejects them).DocumentMetadata.ownerIdis a nullable bare Supabase uid (no local FK). - Soft-deleted documents stamp
DocumentMetadata.deletedAtas a tombstone; a purge/reaper removes the document footprint after retention (documentPurge.service+ matching Supabase purge SQL). make dev-local/make dev-backendrunscripts/dev-local-preflight.tsbefore starting processes (env, deps, DB, extension dist); Next fails to resolve@docs.plus/extension-*when extensiondist/is missing.- Preflight env bootstrap: on first run it creates
.env.developmentfrom.env.example. It also writes.env.localthroughlocalizeEnvForHost(REDIS_HOST/SERVER_RESTAPI_URL/SUPABASE_URL → localhost, DATABASE_URL appended from DB_USER/DB_PASSWORD/DB_PORT). It never mutates an existing.env.local— it only notes key names missing vs.env.example. Keep the Docker→localhost mapping inLOCAL_HOST_OVERRIDES, nowhere else. - Preflight Supabase gate: after the :54321 port check it probes
docker exec supabase_db_docsplus_supabase psql … 'select 1 from public.workspaces limit 1'and fails loud (hint:bun --filter @docs.plus/supabase_back reset) on a foreign or unseeded instance. Never auto-rundb resetfrom preflight — the reset decision stays with the developer. Localsupabase migration upis also wrong by design (migrations are remote-only; scripts/ + seed are canonical locally). concurrentlyis a pinned root devDependency consumed viabunxindev-local/dev-backendwith--kill-others-on-fail(one crashed process tears the run down loudly). Do not remove the pin — unpinnedbunx concurrentlyfetches latest from npm at dev start. Hocuspocus REST/WS/worker entrypoints must notexport defaulta Hono app. Bun auto-serves the default export on:3000, which races the webapp (or wins and restart-storms it). The real listener is the explicitBun.serveonAPP_PORT(:4000). That double-listen caused the 2026-08-04make dev-localREST death. Fatal/uncaughtException/bind failures must exit non-zero after drain.process.exit(0)from graceful shutdown masks the crash, so--kill-others-on-failleaves the rest of the stack up and the webapp hitsECONNREFUSEDon REST.SUPABASE_SERVICE_ROLE_KEYin.env.exampleis the Supabase CLI's public local-demo JWT, in the same demo-secret family as the anon key. It lets the admin gateway and worker queue consumers work on a zero-edit clone; cloud/prod replaces it.- Pad
providerStatussaving→syncedafter edits is a ~300ms local timer — not a durable Postgres ACK. Durable persist runs after Hocuspocus debounce (10s idle / 60s max) via the BullMQ worker and confirms withdocument:saved. Reload before that lands restores the prior DB snapshot (content-disappear class). @docs.plus/document-swarm(packages/document-swarm) is a private, never-published internal Bun + Playwright CLI. It drives N seeded browser "Swarm Actors" against a public editable document URL in Demo (paced, headed) or Stress (higher-concurrency, headless) mode. The glossary is inCONTEXT.md§Document swarm. Subcommands areprovision(idempotently seed Supabase auth users via the Admin API intoactors.json) andrun --mode demo|stress --users N --url <doc>. Make sugar isswarm-demo/swarm-stress. Stable invariants: the host allowlist refuses production (local +stage.docs.plus/*.stage.docs.plusonly). Per-host Actor caps (local 20 / stage 10) need--forceto exceed. The Swarm Target must be public and not Read-only/soft-deleted. Actors sign in by injecting the@supabase/ssrchunkedsb-<ref>-auth-tokencookie viacontext.addCookiesbefore navigation (not localStorage, not Google One Tap). Pad writes are non-destructive; never select-all or wipe the doc. Each actor resolves a Write Target / section. Demo contention defaults section-isolated. Optional Shuffle uses a weighted script mix. First Ctrl+C must immediatelybrowser.close()so in-flight Playwright waits abort (do not hang draining actor loops; second SIGINT or ~5s timeout →process.exit(130)). Itseslint.config.jsuses the baseeslint-config/index.jspreset (notlibrary.js) because console is the CLI interface.- Do not hotlink
raw.githubusercontent.comfor demo or media. That host is a source viewer, not a CDN, and it answers429. Serve demo files from the app (/demo-assets/…on docs.plus). Unknown production paths can return 200 HTML, so checkContent-Type, not status. GitHub raw503/429on extension README images can also be a short outage — do not convertextensions/extension-*/README.md<picture>blocks to markdown because of it. Wait and re-check the same URL. The shipped form stays HTML<picture>with a light PNG and a darksource. - Backend, REST, and operations work is filed in
BACKLOG-hocuspocus-backend.mdunderNotes/local-docs/(gitignored). Public rows are issues #153–#169. - CodeQL default setup is not configured. The live security gate is
bun auditplus.github/scripts/audit-gate.ts(FAIL_ON = 'high'). The old Etherpad CodeQL alerts were dismissed. Currentmainnever contained those files. - Production paging is Telegram, not Discord. Grafana, deploy failure, and the runner watchdog post to Telegram. Discord is release notes plus optional push summaries. Push summaries omit
(build):deploy-trigger commits from the list. A push that is only those commits stays silent. Do not gate the whole Discord post onhead_commitalone — a mixed push then still printed the trigger lines. Do not add*(continued)*labels between Discord chunks. scripts/check-ci.tsforcesNODE_ENV=productiononbuild:ci. Bun setsNODE_ENV=developmentonbun run, and that breaksnext buildprerender of/404. After eachbuild:cithe replica deletesapps/webapp/.nextandapps/admin-dashboard/.next.- Prod CI on
pull_requesttomaindoes not skip Dependabot. Onlydiscord-activity.ymlskips that actor. A red Dependabot PR is a real gate fail. - Webapp Next stays on Pages Router. Issue #172 is a version bump on
pages/. Do not addapp/. Do not follow the official Supabase Next.js quickstart onto App Router. Admin on Next 16 Pages is the in-repo proof. Vendor App Router gains (Instant Navigations, the 22% request number) are not a reason to rewrite the pad. - Next product APIs moved to rest-api (09-09, issue #246). Next
pages/apikeeps only Health. Validate isPOST /api/email/validate. Status is a Supabaseusers.statuswrite, not a Hono route. Confirm is deleted. Do not rebuild Confirm. Do not add Status to rest-api. Do not retarget Health. Glossary:CONTEXT.md§Product HTTP. Details: webapp §Next Product APIs and hocuspocus §HTTP Modules.