diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 239ec84d136..22352e3ae96 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -131,6 +131,15 @@ If the tag does not point to a commit on `main`, the job stops before installing The canonical coding-agent installation prompt lives in `docs/resources/starter-prompt.md`. Edit that Markdown file instead of placing prompt text in a React component. +Keep conditional platform instructions in focused Markdown files under `docs/resources/prompt-assets/` and link to their raw GitHub URLs from the starter prompt. +The main prompt should tell the coding agent when to load each asset and should not repeat the asset's detailed instructions. +Use one shared immutable commit SHA for every platform-asset URL in a starter-prompt revision. +The contributor who changes any platform asset owns the corresponding pin update. +First commit the updated assets, starter-prompt behavior, and related tests without changing the existing URLs, `promptAssetRevision`, or pinned SHA-256 values. +Then use that commit's SHA in every platform-asset URL, update `promptAssetRevision` and every pinned SHA-256 value in `test/starter-prompt-docs.test.ts`, and commit the repin as one atomic follow-up. +Never mix asset URLs from different revisions or point an asset URL at a commit that predates its content. +The exact-revision test compares each local asset byte-for-byte with its Git blob at `promptAssetRevision`, so the intermediate content commit intentionally fails until the atomic repin follow-up points every URL, revision, and digest at that content commit. +Updating only a local digest does not prove what the pinned revision contains. Downstream consumers can pin the source with a raw URL such as `https://raw.githubusercontent.com/NVIDIA/NemoClaw//docs/resources/starter-prompt.md`. The Markdown SPDX comment is part of that raw file but does not appear when Markdown is rendered. diff --git a/docs/resources/prompt-assets/dgx-spark.md b/docs/resources/prompt-assets/dgx-spark.md new file mode 100644 index 00000000000..8e9efd4c7e7 --- /dev/null +++ b/docs/resources/prompt-assets/dgx-spark.md @@ -0,0 +1,30 @@ + + +# DGX Spark Express Instructions + +Use these instructions only after hardware detection confirms DGX Spark. + +Explain that Express keeps the selected agent, uses local vLLM with the default Qwen model, leaves optional setup at its defaults, and downloads the vLLM container and model. +Include the third-party-software notice, then ask: "Run Express install with these settings?" +Choices: + +1. Yes, use the DGX Spark Express defaults. +2. No, let me choose the runtime and model. + +If Express is selected: + +- Set `NEMOCLAW_PROVIDER=install-vllm`. +- Leave `NEMOCLAW_VLLM_MODEL` and `NEMOCLAW_MODEL` unset so the installed release selects its DGX Spark default, currently `nvidia/Qwen3.6-35B-A3B-NVFP4`. +- Set `NEMOCLAW_AGENT` to the agent already selected in the starter prompt. +- Set `NEMOCLAW_NON_INTERACTIVE=1`, `NEMOCLAW_NON_INTERACTIVE_SUDO_MODE=prompt`, `NEMOCLAW_YES=1`, and `NEMOCLAW_POLICY_MODE=suggested`. +- Set `NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1` when Express is accepted. +- Leave `NEMOCLAW_SANDBOX_NAME`, `NEMOCLAW_POLICY_TIER`, web-search settings, and messaging settings unset so the installer applies the remaining Express defaults. +- Treat the Express confirmation as approval for the disclosed notice, downloads, and installation, and skip the later final-permission prompt. +- Do not ask again for the agent or ask separate questions for model, sandbox name, web search, messaging, policy, download approval, or final installation approval. +- After installation, report the model selected by the installed release. + +If Express is declined, continue with the normal provider selection. +Offer existing vLLM when a ready server is detected, managed vLLM, supported local Ollama, and every hosted or compatible provider supported by the selected agent. diff --git a/docs/resources/prompt-assets/dgx-station.md b/docs/resources/prompt-assets/dgx-station.md new file mode 100644 index 00000000000..fd26167e689 --- /dev/null +++ b/docs/resources/prompt-assets/dgx-station.md @@ -0,0 +1,54 @@ + + +# DGX Station DeepSeek Instructions + +Use these instructions only after hardware detection confirms DGX Station. + +The provider-preseeded DeepSeek path below is not the installer Express path and is allowed only on an already-prepared DGX Station. + +Before asking for consent, explain all of these boundaries: + +- The official `--station-deepseek` installer flow may install or change NVIDIA open driver `610.43.02`, Docker CE `29.6.1` with Buildx, NVIDIA Container Toolkit `1.19.1`, and the reviewed factory `dkms` transition from `3.0.11-1ubuntu13` to `1:3.4.0-1ubuntu1`. +- Official Station preparation may add the trusted local account to the `docker` group, which grants root-equivalent control and is suitable only for a trusted single-user development host. +- Official Station preparation may require an operator-controlled reboot and resumes only with the exact accepted NemoClaw revision. +- Both Station paths may install Node.js and the NemoClaw CLI, download a pinned vLLM container and DeepSeek V4 Flash model data, require enough space on the model-cache filesystem and Docker storage, and create a sandbox with suggested policy defaults. +- DGX Station remains an evaluation path with deferred end-to-end validation on physical hardware, so startup may still fail after readiness checks. + +Both `--check` and `--verify` are non-repairing readiness modes, and neither applies host repairs. +`--verify` requires the pinned acceptance image to already be present locally and fails if the image is missing; it does not pull the image. +`--verify` is not read-only: it starts short-lived GPU test containers through both CDI (`nvidia.com/gpu=all`) and Docker `--gpus all`, consumes GPU and temporary Docker storage, and may create Docker state and logs. + +Ask permission to run the selected maintained release's `scripts/prepare-dgx-station-host.sh --check` and `scripts/prepare-dgx-station-host.sh --verify` readiness modes. +Treat the host as prepared only when both modes succeed and confirm the exact Station GB300 platform, generic Ubuntu 24.04 ARM64 image, pinned driver and package versions, packaged CDI lifecycle, `nvidia.com/gpu=all`, and real CDI and `--gpus all` container access. + +If either readiness mode fails, reports a mismatch, is unavailable, or has an inconclusive outcome: + +- Do not set `NEMOCLAW_PROVIDER`, `NEMOCLAW_VLLM_MODEL`, `NEMOCLAW_MODEL`, `NEMOCLAW_NON_INTERACTIVE`, `NEMOCLAW_YES`, or `NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE`. +- Explain that the only supported next step is the official installer with `--station-deepseek`, which owns pinned Station preparation and exact-revision reboot resume, then ask permission to start it. +- Let the official installer present its third-party-software notice and complete confirmation summary. +- Keep the official confirmation visible as the single active question, wait for the user's response, and do not pre-answer or suppress it. +- If a secure interactive terminal is unavailable, stop instead of falling back to the provider-preseeded path. + +If and only if both readiness modes succeed, explain that the prepared-host path skips Station host preparation, uses DeepSeek V4 Flash in vLLM, and downloads the pinned vLLM container and model data. +Include the third-party-software notice, then ask: "Run the prepared-host DeepSeek install with these settings?" +Choices: + +1. Yes, use the prepared-host DeepSeek defaults. +2. No, let me choose the runtime and model. + +If the prepared-host DeepSeek path is selected: + +- Set `NEMOCLAW_PROVIDER=install-vllm`. +- Set `NEMOCLAW_VLLM_MODEL=deepseek-v4-flash` and `NEMOCLAW_MODEL=deepseek-ai/DeepSeek-V4-Flash`. +- Set `NEMOCLAW_AGENT` to the agent already selected in the starter prompt. +- Set `NEMOCLAW_NON_INTERACTIVE=1`, `NEMOCLAW_NON_INTERACTIVE_SUDO_MODE=prompt`, `NEMOCLAW_YES=1`, and `NEMOCLAW_POLICY_MODE=suggested`. +- Set `NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1` when the prepared-host DeepSeek path is accepted. +- Leave `NEMOCLAW_SANDBOX_NAME`, `NEMOCLAW_POLICY_TIER`, web-search settings, and messaging settings unset so the installer applies the remaining maintained defaults. +- Treat the prepared-host confirmation as approval for the disclosed downloads, sandbox creation, and installation, and skip the later final-permission prompt. +- Do not ask again for the agent or ask separate questions for model, sandbox name, web search, messaging, policy, download approval, or final installation approval. + +If the prepared-host DeepSeek path is declined, continue with the normal provider selection. +Offer existing vLLM when a ready server is detected, managed vLLM, supported local Ollama, and every hosted or compatible provider supported by the selected agent. diff --git a/docs/resources/prompt-assets/windows-wsl.md b/docs/resources/prompt-assets/windows-wsl.md new file mode 100644 index 00000000000..a1574327985 --- /dev/null +++ b/docs/resources/prompt-assets/windows-wsl.md @@ -0,0 +1,30 @@ + + +# Windows WSL Express Instructions + +Use these instructions only after official detection identifies Windows WSL. + +Offer the maintained Windows Express path before the normal provider menu. +Explain that Express keeps the selected agent, uses Windows-host Ollama with its memory-aware default model, leaves optional setup at its defaults, and installs or starts Ollama and downloads the model when needed. +Include the third-party-software notice, then ask: "Run Express install with these settings?" +Choices: + +1. Yes, use the Windows WSL Express defaults. +2. No, let me choose the runtime and model. + +If Express is selected: + +- Set `NEMOCLAW_PROVIDER=install-windows-ollama`. +- Leave `NEMOCLAW_MODEL` unset so the installed release chooses its memory-aware Ollama model. +- Set `NEMOCLAW_AGENT` to the agent already selected in the starter prompt. +- Set `NEMOCLAW_NON_INTERACTIVE=1`, `NEMOCLAW_NON_INTERACTIVE_SUDO_MODE=prompt`, `NEMOCLAW_YES=1`, and `NEMOCLAW_POLICY_MODE=suggested`. +- Set `NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1` when Express is accepted. +- Leave `NEMOCLAW_SANDBOX_NAME`, `NEMOCLAW_POLICY_TIER`, web-search settings, and messaging settings unset so the installer applies the remaining Express defaults. +- Treat the Express confirmation as approval for the disclosed notice, downloads, and installation, and skip the later final-permission prompt. +- Do not ask again for the agent or ask separate questions for model, sandbox name, web search, messaging, policy, download approval, or final installation approval. +- Do not start a second Ollama service on the same port. + +If Express is declined, continue with the normal provider selection and offer every provider supported by the selected agent on Windows WSL. diff --git a/docs/resources/starter-prompt.md b/docs/resources/starter-prompt.md index 894194121fe..e0c0f595890 100644 --- a/docs/resources/starter-prompt.md +++ b/docs/resources/starter-prompt.md @@ -13,7 +13,8 @@ I do not know how to use a terminal. - Ask exactly one question at a time. - Use clickable choices when supported; otherwise show one short numbered list and wait. -- Start by asking: "What computer are you using?" Choices: macOS, Windows, Linux. +- Detect the operating system and whether it is WSL using read-only checks. +- Ask which computer I am using only if the environment cannot be determined reliably. - Next ask which agent I want: OpenClaw, Hermes, or LangChain Deep Agents Code. - Never ask me to run commands myself, except the one workstation-side `ssh -N -L` command needed to open a remote credential form securely. - Explain each command in plain language, ask permission, then run it for me. @@ -47,7 +48,8 @@ Use `NEMOCLAW_AGENT=langchain-deepagents-code` or `nemo-deepagents onboard` for - On Linux, ask permission to run a read-only readiness check before provider selection. - Check distribution, architecture, product and firmware identity, GPU and memory, NVIDIA driver, Container Toolkit, Docker, Node.js, disk space, existing NemoClaw, Ollama, vLLM, relevant ports, and administrator access. - Classify the computer as DGX Spark, DGX Station, NVIDIA GB300, another NVIDIA computer, ordinary macOS/Linux, or unknown. -- Do not identify DGX Spark or DGX Station from the GPU name alone; combine product, firmware, architecture, and GPU evidence. +- Do not identify DGX Spark from the GPU name alone; combine product, firmware, architecture, and GPU evidence. +- Classify a system as DGX Station when its firmware identifies a Station GB300 platform, or when its exact OEM model is documented by NVIDIA or the manufacturer as based on DGX Station architecture. - A confirmed NVIDIA GB300 can independently qualify for expanded local-runtime choices. - If uncertain, explain that and let NemoClaw's official preflight make the final platform decision. @@ -62,53 +64,20 @@ Use `NEMOCLAW_AGENT=langchain-deepagents-code` or `nemo-deepagents onboard` for - Offer a user-local alternative only when official documentation supports it for that exact operation. - Do not silently use user-local Ollama for a system Ollama upgrade when the old system service would remain active. -## DGX Express Install +## Platform-Specific Instructions -If DGX Spark or DGX Station is detected, ask: "Do you want the recommended Express Install?" -Choices: - -1. Yes, use the platform's Express model and required Balanced policy. -2. No, let me choose the runtime and model. - -If DGX Spark Express is selected: - -- Use managed vLLM and set `NEMOCLAW_PROVIDER=install-vllm`. -- Leave `NEMOCLAW_VLLM_MODEL` unset so the installed maintained release selects its current Spark Express model. -- Explain container and model download sizes before asking permission. -- Report the model selected by the installed release. - -If DGX Station Express is selected: - -- Use managed vLLM. -- Explicitly select `nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-NVFP4`. -- Do not leave the model unset; the ordinary managed-vLLM default can select DeepSeek and would not reproduce Express. -- Set `NEMOCLAW_PROVIDER=install-vllm`. -- Set `NEMOCLAW_VLLM_MODEL=nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-NVFP4`. -- Disclose that the model download is approximately 352 GB, in addition to the vLLM container and temporary download space. -- Verify the model-cache filesystem and Docker storage have sufficient capacity. -- Warn that DGX Station managed deployment has deferred end-to-end physical-hardware validation. -- Describe it as an evaluation path, not a validated production deployment. -- Explain that startup may fail despite passing initial checks. -- Ask separately for approval of the approximately 352 GB download. - -For both Express paths: - -- Balanced policy is required for Express; set `NEMOCLAW_POLICY_TIER=balanced`, `NEMOCLAW_NON_INTERACTIVE=1`, and the selected `NEMOCLAW_AGENT`. -- Set `NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1` only after explaining the notice and receiving approval. -- Set `NEMOCLAW_YES=1` only after both the separate download approval and final install approval. -- Set `NEMOCLAW_NON_INTERACTIVE_SUDO_MODE=prompt` only when required and a secure sudo prompt is available. -- Ask separately for sandbox name, web search, messaging when the selected agent supports it, download approval, and final install approval. +After the readiness check, load exactly one matching instruction asset before provider selection: -## Windows WSL Express Install +- Confirmed DGX Spark: [DGX Spark Express instructions](https://raw.githubusercontent.com/NVIDIA/NemoClaw/f3682a5be7069e58303d3345e682424d5c2453b2/docs/resources/prompt-assets/dgx-spark.md). +- Confirmed DGX Station: [DGX Station installation instructions](https://raw.githubusercontent.com/NVIDIA/NemoClaw/f3682a5be7069e58303d3345e682424d5c2453b2/docs/resources/prompt-assets/dgx-station.md). +- Officially detected Windows WSL: [Windows WSL Express instructions](https://raw.githubusercontent.com/NVIDIA/NemoClaw/f3682a5be7069e58303d3345e682424d5c2453b2/docs/resources/prompt-assets/windows-wsl.md). -If official detection identifies Windows WSL, offer the maintained Windows Express path before the normal provider menu. -Explain that it uses Windows-host Ollama through Docker Desktop WSL integration. -If selected, set `NEMOCLAW_PROVIDER=install-windows-ollama`, collect the same separate approvals, and let the installed release choose its maintained Ollama model. -Do not start a second Ollama service on the same port. +Read the matching raw Markdown file completely and follow it before continuing. +Do not load a platform asset for any other computer. ## Runtime and Provider Selection -If Express is declined on DGX Spark, DGX Station, or GB300, ask: "Which inference runtime or provider would you like?" +If no platform asset applies, or its offered install path is declined, ask: "Which inference runtime or provider would you like?" Choices: 1. Existing vLLM, only when a ready server is detected on `localhost:8000`. @@ -133,6 +102,7 @@ On ordinary supported macOS or Linux: - Do not hide Ollama merely because the computer is not DGX or GB300. - Omit managed vLLM unless current official support permits it for the detected hardware. +When a platform asset applies, follow its local-runtime eligibility and model instructions. On other platforms, show every provider supported by the selected agent and platform. Renumber choices after filtering and do not hide hosted providers behind another menu. Ask required model, endpoint, credential, and download questions one at a time. @@ -141,7 +111,6 @@ Ask required model, endpoint, credential, and download questions one at a time. - Fetch current model choices from the selected agent's official Markdown documentation. - The selected maintained NemoClaw release is authoritative for supported slugs and arguments. -- Managed-vLLM examples include `qwen3.6-27b`, `qwen3.6-35b-a3b-nvfp4`, `nemotron-3-nano-4b`, `deepseek-v4-flash`, and gated `deepseek-r1-distill-70b`. - For Ollama, ask permission to inspect installed models and offer NemoClaw's memory-aware recommendation first. - Current Ollama starter examples include `qwen3.6:35b`, `nemotron-3-nano:30b`, and `qwen3.5:9b`. - Explain download size and storage requirements, then ask separately for permission. @@ -150,7 +119,7 @@ Ask required model, endpoint, credential, and download questions one at a time. ## Avoid Interactive Menus - Collect every choice before running the installer. -- Ask one question at a time for model, endpoint, sandbox name, web search, messaging when the selected agent supports it, policy when Express is not selected, credentials, administrator access, and downloads. +- Ask one question at a time for model, endpoint, sandbox name, web search, messaging when the selected agent supports it, policy when no platform-asset install path is selected, credentials, administrator access, and downloads. - Use non-interactive environment variables whenever supported. - Never leave a command waiting at `Choose [1]:`. - If a choice cannot be supplied non-interactively, stop before starting and explain the supported alternative. @@ -200,8 +169,7 @@ Use this provider mapping for non-interactive setup: - Anthropic-compatible: `NEMOCLAW_PROVIDER=anthropicCompatible`, endpoint, model, `COMPATIBLE_ANTHROPIC_API_KEY`. - Ollama: `NEMOCLAW_PROVIDER=ollama`, optional `NEMOCLAW_MODEL`. - Existing vLLM: `NEMOCLAW_PROVIDER=vllm`. -- Managed vLLM: `NEMOCLAW_PROVIDER=install-vllm`; leave `NEMOCLAW_VLLM_MODEL` unset for DGX Spark Express, set it to `nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-NVFP4` for DGX Station Express, or use an approved optional override for non-Express setup. -- Windows WSL Express: `NEMOCLAW_PROVIDER=install-windows-ollama`. +- Managed vLLM: `NEMOCLAW_PROVIDER=install-vllm`; use an approved optional model override only when the selected platform supports it. Do not offer Hermes Provider for OpenClaw or Deep Agents. @@ -247,11 +215,12 @@ Use `channels add` and rebuild only for channels omitted from initial onboarding ## Policy, Approval, and Verification -- For Express, state that Balanced policy is required, keep `NEMOCLAW_POLICY_TIER=balanced`, and skip the policy-tier question. -- For non-Express installation, ask for Balanced, Restricted, or Open policy. +- If a loaded platform asset selects its approved install path, follow its policy requirement and skip the policy-tier question. +- For installation outside an accepted platform-asset path, ask for Balanced, Restricted, or Open policy. - Explain that messaging and web-search selections add required endpoints. -- Before installation, summarize platform, administrator access, agent, Express choice, provider, exact model, validation warning, downloads, storage, sandbox, web search, messaging, policy, credential names without their values, and system changes. -- Ask for final permission. +- Before installation outside an accepted platform-asset path, summarize platform, administrator access, agent, provider, exact model, validation warning, downloads, storage, sandbox, web search, messaging, policy, credential names without their values, and system changes. +- Ask for final permission before installation outside an accepted platform-asset path. +- For an accepted platform-asset install path, treat the asset's confirmation as final permission and do not ask again. - Set `NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1` and `NEMOCLAW_YES=1` only after their approvals. - Keep credentials in the approved environment and never display them. - Verify the command and version, sandbox status, provider, model, `inference.local`, GPU access when applicable, messaging bridges when configured, and dashboard route when available. diff --git a/test/helpers/starter-prompt-asset-contract.ts b/test/helpers/starter-prompt-asset-contract.ts new file mode 100644 index 00000000000..5f7a9d1f23e --- /dev/null +++ b/test/helpers/starter-prompt-asset-contract.ts @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { spawnSync } from "node:child_process"; + +type PromptAssetPath = { + path: string; +}; + +type PromptAssetRoute = { + asset: { url: string }; + label: string; +}; + +export type GitResult = { + status: number | null; + stdout: Buffer; +}; + +export type GitRunner = (args: readonly string[], timeoutMs?: number) => GitResult; + +export function createGitRunner(repoRoot: string): GitRunner { + return (args, timeoutMs = 10_000) => { + const result = spawnSync("git", [...args], { + cwd: repoRoot, + env: { ...process.env, GIT_NO_REPLACE_OBJECTS: "1" }, + maxBuffer: 4 * 1024 * 1024, + timeout: timeoutMs, + }); + return { + status: result.status, + stdout: Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout ?? ""), + }; + }; +} + +export function resolvePromptAssetRevision(revision: string, git: GitRunner): void { + if (!/^[0-9a-f]{40}$/.test(revision)) { + throw new Error("promptAssetRevision must be a full lowercase commit SHA"); + } + + let revisionType = git(["cat-file", "-t", revision]); + if (revisionType.status !== 0) { + const fetch = git( + [ + "fetch", + "--no-tags", + "--no-recurse-submodules", + "--no-write-fetch-head", + "--depth=1", + "origin", + revision, + ], + 120_000, + ); + if (fetch.status !== 0) { + throw new Error(`could not fetch immutable prompt asset revision ${revision}`); + } + revisionType = git(["cat-file", "-t", revision]); + } + + if (revisionType.status !== 0) { + throw new Error(`immutable prompt asset revision ${revision} is unavailable after fetch`); + } + const objectType = revisionType.stdout.toString("utf8").trim(); + if (objectType !== "commit") { + throw new Error( + `promptAssetRevision must resolve to a commit object, got ${objectType || "no type"}`, + ); + } +} + +export function readPinnedPromptAssetBlob( + revision: string, + asset: PromptAssetPath, + git: GitRunner, +): Buffer { + const tree = git(["ls-tree", "-z", revision, "--", asset.path]); + if (tree.status !== 0) { + throw new Error(`${revision} prompt asset tree could not be read for ${asset.path}`); + } + const entry = + /^(?100644|100755) blob (?[0-9a-f]{40}|[0-9a-f]{64})\t(?[^\0]+)\0$/u.exec( + tree.stdout.toString("utf8"), + ); + if (!entry?.groups || entry.groups.path !== asset.path) { + throw new Error( + `${revision} must contain exactly one regular prompt asset blob at ${asset.path}`, + ); + } + + const blobType = git(["cat-file", "-t", entry.groups.oid]); + if (blobType.status !== 0 || blobType.stdout.toString("utf8").trim() !== "blob") { + throw new Error(`${revision}:${asset.path} does not resolve to a readable Git blob`); + } + const blob = git(["cat-file", "blob", entry.groups.oid]); + if (blob.status !== 0) { + throw new Error(`could not read immutable prompt asset blob ${revision}:${asset.path}`); + } + return blob.stdout; +} + +function promptAssetRoutesIn(content: string): Map { + const heading = "## Platform-Specific Instructions\n"; + const headingIndex = content.indexOf(heading); + if (headingIndex < 0) throw new Error("Missing Platform-Specific Instructions section"); + const bodyStart = headingIndex + heading.length; + const nextHeading = content.indexOf("\n## ", bodyStart); + const section = content.slice(bodyStart, nextHeading < 0 ? content.length : nextHeading); + const routes = new Map(); + + for (const match of section.matchAll( + /^- (?