Appearance
Docs Wiki Implementation Plan ​
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: A VitePress wiki over the repo's docs/ markdown, added as an Nx project, with instant HMR, live sidebar updates when a new .md is dropped in, built-in search, and a deployable (Cloudflare Pages behind Cloudflare Access) static build.
Architecture: VitePress is rooted at the existing repo-root docs/ directory; markdown stays in place. A small hand-written scanner (docs/.vitepress/sidebar.ts) builds the sidebar from the tree at config-load, anchored to its own directory via import.meta.url (cwd-independent, so it's correct under Nx's cwd: docs). A ~20-line Vite plugin restarts the dev server on .md add/remove so new files appear in the sidebar live. Nx nx:run-commands targets wrap vitepress dev/build/preview. Deploy mirrors the existing web deploy.
Tech Stack: VitePress (dev dependency, installed with bun), Vite (transitive), Nx nx:run-commands, bun test (built-in) for the scanner, Cloudflare Pages + Cloudflare Access, GitHub Actions.
Global Constraints ​
- Package manager is bun. Install with
bun add, run scripts withbunx/bun. Nevernpm/npx/yarn. - Run tasks through Nx where a target exists:
bunx nx serve docs,bunx nx build docs. - Content scope:
docs/**/*.mdonly. Never serve non-markdown. Explicitly excludedocs/implementation/contract-qa/**. The sensitivedocs/google_client_secret*.jsonanddocs/samples/*.pdfare non-markdown and are never served — do not add config that would surface them. - cwd-independence: the sidebar scanner MUST resolve paths from
import.meta.url, neverprocess.cwd(). - Deploy is gated. The Cloudflare Pages site MUST sit behind a Cloudflare Access application with an explicit email allow-list (not a whole-domain rule):
damien@seeg.dev,damien@seeg.io,damien@seeg.co,damiens@gmail.com,damien@damien.in. Never promote the deploy workflow topush:before the gate is confirmed. - Do not push / open a PR until the batch is locally green and the reviewer signs off (repo push discipline). Commit locally per task.
- Brand accent derives from Dispatch primary
#C1D841and primary-content#10130B. - Branch:
feature/docs-wiki(already checked out).
Task 1: VitePress skeleton as an Nx project ​
Stand up a minimal, buildable VitePress site rooted at docs/, registered as the Nx project docs. No custom sidebar yet — this task proves the toolchain (install, serve, build, Nx wiring) end to end.
Files:
- Modify:
package.json(addsvitepresstodevDependencies— done bybun add) - Create:
docs/.vitepress/config.mts - Create:
docs/index.md - Create:
docs/project.json - Modify:
.gitignore(append VitePress ignores)
Interfaces:
Consumes: nothing.
Produces: Nx project
docswith targetsserve,build,preview; VitePress config default export fromdocs/.vitepress/config.mts.[ ] Step 1: Install VitePress
Run:
bash
bun add -d vitepressExpected: vitepress appears under devDependencies in package.json; bun.lock updates.
- [ ] Step 2: Write the minimal VitePress config
Create docs/.vitepress/config.mts:
ts
import { defineConfig } from 'vitepress'
// Rooted at the repo-root docs/ directory. The dev server and build run with
// cwd: docs (see docs/project.json), so root defaults to docs/.
export default defineConfig({
title: 'Dispatch Docs',
description: 'Internal documentation for the Dispatch monorepo',
// contract-qa is a vendored prototype tree, not documentation.
srcExclude: ['**/contract-qa/**'],
// Existing docs were not authored for VitePress routing; keep the build green.
ignoreDeadLinks: true,
themeConfig: {
nav: [{ text: 'Home', link: '/' }],
search: { provider: 'local' },
outline: { level: [2, 3] },
},
})- [ ] Step 3: Write the landing page
Create docs/index.md:
md
---
layout: home
hero:
name: Dispatch Docs
tagline: Internal documentation for the Dispatch monorepo. Browse from the sidebar or search.
features:
- title: Overview
details: What Dispatch is and how the pieces fit together.
link: /dispatch-overview
- title: Production Design
details: Architecture and the Angular handoff.
link: /dispatch-production-design
- title: Auth
details: Auth setup and the authorization matrix.
link: /auth-setup
---- [ ] Step 4: Register the Nx project
Create docs/project.json:
json
{
"name": "docs",
"$schema": "../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "docs",
"targets": {
"serve": {
"executor": "nx:run-commands",
"options": { "command": "bunx vitepress dev", "cwd": "docs" }
},
"build": {
"executor": "nx:run-commands",
"options": { "command": "bunx vitepress build", "cwd": "docs" },
"outputs": ["{projectRoot}/.vitepress/dist"],
"cache": true
},
"preview": {
"executor": "nx:run-commands",
"options": { "command": "bunx vitepress preview", "cwd": "docs" }
}
}
}- [ ] Step 5: Ignore VitePress build artifacts
Append to .gitignore:
gitignore
# VitePress docs site
docs/.vitepress/dist
docs/.vitepress/cache- [ ] Step 6: Verify Nx sees the project
Run:
bash
bunx nx show projects | grep -x docsExpected: prints docs.
- [ ] Step 7: Verify the build succeeds
Run:
bash
bunx nx build docsExpected: VitePress builds; output written to docs/.vitepress/dist (contains index.html). If it fails on a page with literal {{ … }} outside a code fence, wrap that block in <div v-pre> … </div> in the offending .md, or add the page to srcExclude, then re-run. Note each such change in the commit message.
- [ ] Step 8: Verify serve runs
Run (starts a dev server; stop it with Ctrl-C after confirming):
bash
bunx nx serve docsExpected: prints a local URL (e.g. http://localhost:5173/); opening it shows the "Dispatch Docs" home page. Stop the server.
- [ ] Step 9: Commit
bash
git add package.json bun.lock docs/.vitepress/config.mts docs/index.md docs/project.json .gitignore
git commit -m "feat(docs): scaffold VitePress site as Nx project 'docs'"Task 2: Hand-written sidebar scanner (TDD) ​
Build the sidebar from the docs tree, cwd-independent, unit-tested. Then wire it into the config.
Files:
- Create:
docs/.vitepress/sidebar.ts - Create:
docs/.vitepress/sidebar.test.ts - Modify:
docs/.vitepress/config.mts
Interfaces:
Consumes:
defineConfigfrom Task 1's config.Produces:
titleFromContent(md: string): string | null— first# H1of a markdown body (frontmatter stripped), elsenull.prettifyName(slug: string): string— filename/folder slug → Title Case (-/_→ space,.mdstripped).interface SidebarItem { text: string; link?: string; items?: SidebarItem[]; collapsed?: boolean }.buildSidebar(root?: string): SidebarItem[]— nested sidebar;rootdefaults to the docs directory (parent of.vitepress).
[ ] Step 1: Write the failing test
Create docs/.vitepress/sidebar.test.ts:
ts
import { test, expect } from 'bun:test'
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { titleFromContent, prettifyName, buildSidebar } from './sidebar'
test('titleFromContent returns the first H1, stripping frontmatter', () => {
expect(titleFromContent('# Hello world\n\nbody')).toBe('Hello world')
expect(titleFromContent('---\ntitle: x\n---\n\n# Real Title\n')).toBe('Real Title')
expect(titleFromContent('no heading here')).toBeNull()
})
test('prettifyName title-cases slugs', () => {
expect(prettifyName('auth-setup')).toBe('Auth Setup')
expect(prettifyName('COST_OPTIMIZATION')).toBe('Cost Optimization')
expect(prettifyName('overview.md')).toBe('Overview')
})
test('buildSidebar groups folders, sorts dates newest-first, excludes contract-qa and index', () => {
const root = mkdtempSync(join(tmpdir(), 'docs-'))
writeFileSync(join(root, 'index.md'), '# Home')
writeFileSync(join(root, 'overview.md'), '# Overview\n')
mkdirSync(join(root, 'reviews'))
writeFileSync(join(root, 'reviews', '2026-01-01-a.md'), '# Review A')
writeFileSync(join(root, 'reviews', '2026-02-02-b.md'), '# Review B')
mkdirSync(join(root, 'implementation', 'contract-qa'), { recursive: true })
writeFileSync(join(root, 'implementation', 'contract-qa', 'README.md'), '# Vendored')
const sb = buildSidebar(root)
// top-level file, titled by its H1, index.md excluded
expect(sb.find((i) => i.link === '/overview')?.text).toBe('Overview')
expect(sb.some((i) => i.link === '/index')).toBe(false)
// reviews group, newest date first
const reviews = sb.find((i) => i.text === 'Reviews')
expect(reviews?.collapsed).toBe(true)
expect(reviews?.items?.map((i) => i.text)).toEqual(['Review B', 'Review A'])
// implementation contained only contract-qa -> empty -> omitted
expect(sb.find((i) => i.text === 'Implementation')).toBeUndefined()
})- [ ] Step 2: Run the test to verify it fails
Run:
bash
bun test docs/.vitepress/sidebar.test.tsExpected: FAIL — module ./sidebar cannot be resolved / exports missing.
- [ ] Step 3: Write the scanner
Create docs/.vitepress/sidebar.ts:
ts
import { readdirSync, readFileSync } from 'node:fs'
import { join, relative, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
export interface SidebarItem {
text: string
link?: string
items?: SidebarItem[]
collapsed?: boolean
}
// Anchor to THIS file's directory, never process.cwd(): docs/ is the parent of
// docs/.vitepress/. Correct regardless of where Nx runs the command from.
const DOCS_ROOT = fileURLToPath(new URL('..', import.meta.url))
const SKIP_DIRS = new Set(['.vitepress', 'node_modules', 'samples'])
const CONTRACT_QA = /(^|[\\/])contract-qa([\\/]|$)/
export function titleFromContent(md: string): string | null {
const body = md.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '')
const m = body.match(/^#\s+(.+?)\s*$/m)
return m ? m[1].trim() : null
}
export function prettifyName(slug: string): string {
return slug
.replace(/\.md$/i, '')
.replace(/[-_]+/g, ' ')
.trim()
.split(/\s+/)
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
.join(' ')
}
function isDateNamed(name: string): boolean {
return /^\d{4}-\d{2}-\d{2}/.test(name)
}
function titleForFile(absPath: string, fileName: string): string {
try {
const t = titleFromContent(readFileSync(absPath, 'utf8'))
if (t) return t
} catch {
// unreadable file → fall back to the prettified name
}
return prettifyName(fileName)
}
export function buildSidebar(root: string = DOCS_ROOT): SidebarItem[] {
function walk(dir: string): SidebarItem[] {
const entries = readdirSync(dir, { withFileTypes: true })
const dirs = entries
.filter((e) => e.isDirectory() && !SKIP_DIRS.has(e.name))
.map((e) => e.name)
.sort((a, b) => a.localeCompare(b))
const files = entries
.filter(
(e) =>
e.isFile() &&
e.name.toLowerCase().endsWith('.md') &&
e.name.toLowerCase() !== 'index.md',
)
.map((e) => e.name)
.sort((a, b) =>
isDateNamed(a) && isDateNamed(b) ? b.localeCompare(a) : a.localeCompare(b),
)
const items: SidebarItem[] = []
for (const name of dirs) {
const abs = join(dir, name)
if (CONTRACT_QA.test(relative(root, abs))) continue
const children = walk(abs)
if (children.length) {
items.push({ text: prettifyName(name), collapsed: true, items: children })
}
}
for (const name of files) {
const abs = join(dir, name)
const rel = relative(root, abs).split(sep).join('/')
if (CONTRACT_QA.test(rel)) continue
items.push({ text: titleForFile(abs, name), link: '/' + rel.replace(/\.md$/i, '') })
}
return items
}
return walk(root)
}- [ ] Step 4: Run the test to verify it passes
Run:
bash
bun test docs/.vitepress/sidebar.test.tsExpected: PASS — 3 tests pass.
- [ ] Step 5: Wire the sidebar into the config
In docs/.vitepress/config.mts, add the import at the top:
ts
import { buildSidebar } from './sidebar'and set the sidebar inside themeConfig (replace the existing themeConfig block):
ts
themeConfig: {
nav: [{ text: 'Home', link: '/' }],
sidebar: buildSidebar(),
search: { provider: 'local' },
outline: { level: [2, 3] },
},- [ ] Step 6: Verify the build still succeeds with the real sidebar
Run:
bash
bunx nx build docsExpected: build succeeds. (Apply the same <div v-pre> / srcExclude remediation from Task 1 Step 7 if any page fails.)
- [ ] Step 7: Verify the sidebar content by eye
Run:
bash
bunx nx serve docsExpected: the left sidebar lists top-level docs (e.g. "Dispatch Overview") and collapsible groups (Reviews, Implementation, Superpowers, Tracking, …); no contract-qa entries appear. Stop the server.
- [ ] Step 8: Commit
bash
git add docs/.vitepress/sidebar.ts docs/.vitepress/sidebar.test.ts docs/.vitepress/config.mts
git commit -m "feat(docs): auto-generated sidebar from the docs tree (bun-tested)"Task 3: Live sidebar on file add/remove ​
Make a brand-new .md appear in the sidebar without a manual restart.
Files:
- Create:
docs/.vitepress/live-sidebar.ts - Modify:
docs/.vitepress/config.mts
Interfaces:
- Consumes:
buildSidebar()(indirectly, via VitePress's config-change restart re-running the config). - Produces:
liveSidebar(): Plugin— a Vite plugin that touchesconfig.mtson.mdadd/unlink to trigger a config-change restart.
Mechanism note (learned during implementation): do NOT call
server.restart()directly. Withsearch.provider: 'local', a plugin-initiatedserver.restart()does not reset the MiniSearch index, so every restart throwsMiniSearch: duplicate ID …→server restart failedand the sidebar never refreshes (vuejs/vitepress#4251). Instead, bump the mtime ofconfig.mts(viautimes, no content change so git stays clean) — VitePress's own config-change watcher then performs a clean, search-index-clearing restart.
- [ ] Step 1: Write the plugin
Create docs/.vitepress/live-sidebar.ts:
ts
import { utimes } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import type { Plugin, ViteDevServer } from 'vite'
// Touching the config file triggers VitePress's OWN config-change restart, which
// (unlike a raw server.restart() from a plugin) clears the local-search MiniSearch
// index — avoiding the "MiniSearch: duplicate ID / server restart failed" bug
// (vuejs/vitepress#4251). On that restart, config.mts re-runs buildSidebar() and
// the added/removed .md appears in the sidebar. Anchored to import.meta.url, not cwd.
const CONFIG_FILE = fileURLToPath(new URL('./config.mts', import.meta.url))
export function liveSidebar(): Plugin {
let timer: ReturnType<typeof setTimeout> | undefined
return {
name: 'dispatch-live-sidebar',
configureServer(server: ViteDevServer) {
const onAddOrRemove = (file: string) => {
if (!file.endsWith('.md')) return
clearTimeout(timer)
timer = setTimeout(() => {
const now = new Date()
// Bump mtime only (no content change → git stays clean). Swallow any
// transient fs error so a one-off failure just skips this refresh
// rather than becoming an unhandled rejection that crashes the server.
void utimes(CONFIG_FILE, now, now).catch(() => undefined)
}, 150)
}
server.watcher.on('add', onAddOrRemove)
server.watcher.on('unlink', onAddOrRemove)
},
}
}- [ ] Step 2: Register the plugin in the config
In docs/.vitepress/config.mts, add the import at the top:
ts
import { liveSidebar } from './live-sidebar'and add a top-level vite field to the defineConfig({ … }) object (sibling of themeConfig):
ts
vite: {
plugins: [liveSidebar()],
},- [ ] Step 3: Verify build is unaffected
Run:
bash
bunx nx build docsExpected: build still succeeds (the plugin only runs on the dev server, not the build).
- [ ] Step 4: Verify live-add behavior manually
Run the dev server, then in a second terminal create a file:
bash
bunx nx serve docsbash
# second terminal:
printf '# Live Add Test\n\nhello\n' > docs/zzz-live-add-test.mdExpected: the server logs a restart; within ~1s the sidebar shows a "Live Add Test" entry (top-level, sorted after other root docs) without a manual restart. Then remove it and confirm it disappears:
bash
rm docs/zzz-live-add-test.mdExpected: the entry disappears after the next restart. Stop the server.
- [ ] Step 5: Commit
bash
git add docs/.vitepress/live-sidebar.ts docs/.vitepress/config.mts
git commit -m "feat(docs): live sidebar refresh on markdown add/remove"Task 4: Brand accent theme ​
Give the site a thin Dispatch accent so it doesn't read as stock VitePress. Colour only; no external fonts.
Files:
- Create:
docs/.vitepress/theme/index.ts - Create:
docs/.vitepress/theme/custom.css
Interfaces:
Consumes: VitePress default theme.
Produces: a custom theme (default export) that extends the default theme and loads
custom.css.[ ] Step 1: Create the theme entry
Create docs/.vitepress/theme/index.ts:
ts
import DefaultTheme from 'vitepress/theme'
import './custom.css'
export default DefaultTheme- [ ] Step 2: Create the brand accent CSS
Create docs/.vitepress/theme/custom.css:
css
/* Thin Dispatch accent over the default VitePress theme.
Derived from the design-system primary (#C1D841) / primary-content (#10130B).
Raw #C1D841 is a surface colour, not a text colour: links use a darkened lime
on light, the raw lime on dark; solid accents keep the exact brand pairing. */
:root {
--vp-c-brand-1: #66761f; /* darkened lime — ~5:1 on white, clears WCAG AA for link text */
--vp-c-brand-2: #566219;
--vp-c-brand-3: #c1d841; /* solid accents / active */
--vp-c-brand-soft: rgba(193, 216, 65, 0.2);
--vp-button-brand-bg: #c1d841;
--vp-button-brand-text: #10130b;
--vp-button-brand-hover-bg: #cfe25f;
--vp-button-brand-hover-text: #10130b;
--vp-button-brand-active-bg: #b3ca34;
--vp-button-brand-active-text: #10130b;
}
.dark {
--vp-c-brand-1: #c1d841; /* lime reads well on the dark base */
--vp-c-brand-2: #cfe25f;
--vp-c-brand-3: #c1d841;
}- [ ] Step 3: Verify the accent renders
Run:
bash
bunx nx serve docsExpected: the hero "get started"-style accents, links, and active sidebar item use the lime/green accent (not VitePress default green-blue); the home hero action button is lime with dark text. Toggle dark mode and confirm links are the brighter lime. Stop the server.
- [ ] Step 4: Verify the build embeds the theme
Run:
bash
bunx nx build docsExpected: build succeeds (the custom theme is bundled).
- [ ] Step 5: Commit
bash
git add docs/.vitepress/theme/index.ts docs/.vitepress/theme/custom.css
git commit -m "feat(docs): Dispatch brand accent theme"Task 5: Deploy artifacts (Cloudflare Pages) ​
Add the Pages config and a manual deploy workflow that builds and publishes the static site. This task produces the deploy artifacts; the Access gate (Task 6) must be in place before the workflow is actually run.
Files:
- Create:
infra/cloudflare/docs.wrangler.toml - Create:
.github/workflows/deploy-docs.yml
Interfaces:
Consumes:
bunx nx build docsoutput atdocs/.vitepress/dist; existing repo secretsCLOUDFLARE_API_TOKEN,CLOUDFLARE_ACCOUNT_ID.Produces: a
dispatch-docsCloudflare Pages deployment on manual dispatch.[ ] Step 1: Add the Pages config
Create infra/cloudflare/docs.wrangler.toml:
toml
# Cloudflare Pages config for the Dispatch docs wiki (VitePress).
# Build: bunx nx build docs
# Deploy: wrangler pages deploy docs/.vitepress/dist --project-name=dispatch-docs
# The site is gated by a Cloudflare Access application (explicit email allow-list).
name = "dispatch-docs"
compatibility_date = "2026-06-15"
pages_build_output_dir = "../../docs/.vitepress/dist"- [ ] Step 2: Add the manual deploy workflow
Create .github/workflows/deploy-docs.yml:
yaml
name: Deploy Docs
# Manual deploy of the VitePress docs wiki to Cloudflare Pages (project
# dispatch-docs). Kept separate from the product deploy.yml so docs and app
# releases stay decoupled. Requires repo secrets CLOUDFLARE_API_TOKEN and
# CLOUDFLARE_ACCOUNT_ID. The Pages site MUST be behind a Cloudflare Access
# application (explicit email allow-list) before this is used — see
# docs/superpowers/specs/2026-07-16-docs-wiki-design.md.
on:
workflow_dispatch:
permissions:
contents: read
jobs:
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.11
- run: bun install --frozen-lockfile
- run: bunx nx build docs
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy docs/.vitepress/dist --project-name=dispatch-docs- [ ] Step 3: Verify the build output path the workflow expects
Run:
bash
bunx nx build docs && test -f docs/.vitepress/dist/index.html && echo OKExpected: prints OK (confirms docs/.vitepress/dist is the correct publish directory referenced by both files).
- [ ] Step 4: Verify the workflow YAML is valid
Run (uses actionlint via bunx if available; otherwise a YAML parse check):
bash
bunx --yes actionlint .github/workflows/deploy-docs.yml || bunx --yes js-yaml .github/workflows/deploy-docs.yml >/dev/null && echo "yaml-ok"Expected: no errors (actionlint clean, or yaml-ok printed).
- [ ] Step 5: Commit
bash
git add infra/cloudflare/docs.wrangler.toml .github/workflows/deploy-docs.yml
git commit -m "feat(docs): Cloudflare Pages deploy artifacts for the docs wiki"Task 6: Cloudflare Access gate (operational) ​
Stand up the Pages project and put it behind Cloudflare Access with the explicit email allow-list. This is an operational task performed against Cloudflare (dashboard or API) plus a verification; it does not change repo code. Do not run the deploy workflow for real until Steps 2–3 are done.
Files: none (Cloudflare-side configuration).
Interfaces:
Consumes: the
deploy-docs.ymlworkflow anddispatch-docsproject name from Task 5.Produces: a gated, reachable-only-by-allow-listed-emails docs site.
[ ] Step 1: Create the Pages project + first deploy
Trigger the Deploy Docs workflow (GitHub → Actions → Deploy Docs → Run workflow) once, OR deploy locally:
bash
bunx nx build docs
bunx wrangler pages deploy docs/.vitepress/dist --project-name=dispatch-docsExpected: a dispatch-docs Pages project exists with a production URL (https://dispatch-docs.pages.dev) and a preview subdomain pattern (*.dispatch-docs.pages.dev).
- [ ] Step 2: Create the Access application
In Cloudflare Zero Trust → Access → Applications → Add → Self-hosted, add an application covering BOTH the production and preview hostnames so preview deployments are gated too:
Application domain 1:
dispatch-docs.pages.devApplication domain 2 (wildcard subdomain):
*.dispatch-docs.pages.dev[ ] Step 3: Add the allow-list policy
Add one policy: Action Allow, rule type Emails, with exactly these addresses:
damien@seeg.dev
damien@seeg.io
damien@seeg.co
damiens@gmail.com
damien@damien.inEverything else is blocked at Cloudflare's edge. Save.
- [ ] Step 4: Verify the gate blocks anonymous access
Run:
bash
curl -sI https://dispatch-docs.pages.dev | grep -iE 'HTTP/|location'Expected: a redirect (HTTP 302) to a *.cloudflareaccess.com login URL — NOT a 200 serving the docs. Repeat against a preview URL if one is available and confirm the same redirect. Then confirm an allow-listed email can log in and view the site in a browser.
- [ ] Step 5: Record the outcome
No code change. In the batch's review notes / STATUS, record that dispatch-docs is deployed and gated, and that deploy-docs.yml stays workflow_dispatch (never promoted to push:) until the gate is reconfirmed after any hostname change.
Self-Review ​
Spec coverage:
- VitePress rooted at
docs/, Nx projectdocs→ Task 1. ✓ - Auto-generated sidebar, cwd-independent, titles from H1, date sort, contract-qa excluded → Task 2. ✓
- Live sidebar on add/remove → Task 3. ✓
- Content scope
docs/**/*.md, sensitive files never served (non-markdown),srcExcludecontract-qa,ignoreDeadLinks→ Tasks 1–2. ✓ - Built-in local search → Task 1 (
search: { provider: 'local' }). ✓ - Brand accent theme from
#C1D841/#10130B→ Task 4. ✓ - Deploy to Cloudflare Pages
dispatch-docs, dedicated manual workflow, mirrors web deploy → Task 5. ✓ - Cloudflare Access gate, explicit email allow-list, prod + preview hostnames → Task 6. ✓
- Vue-interpolation build risk → remediation baked into Task 1 Step 7 / Task 2 Step 6. ✓
bun testfor the scanner, no new deps → Task 2. ✓
Placeholder scan: No TBD/TODO; every code step shows complete code; verification steps have exact commands + expected output. Cloudflare Access (Task 6) is inherently operational — its steps are concrete dashboard actions + a curl assertion, not placeholders.
Type consistency: SidebarItem, titleFromContent, prettifyName, buildSidebar are defined in Task 2 and used consistently; liveSidebar() returns Vite Plugin and is the only symbol Task 3 adds; config imports match the exported names (buildSidebar, liveSidebar). Publish dir docs/.vitepress/dist is identical across Tasks 1, 5, 6. Project name dispatch-docs consistent across Tasks 5–6.