From ab1935edd016809b8ad570ffa6b48f189f6e9bf8 Mon Sep 17 00:00:00 2001 From: Sinduri Guntupalli Date: Tue, 21 Jul 2026 10:52:40 +0200 Subject: [PATCH 1/6] fix(sync): fall back to local src/assets/diagrams/ when SVG is absent from challenges repo - When the challenges repo does not have a diagram at docs/diagrams/, check whether the file already exists in src/assets/diagrams/ on disk - If found locally, add it to fetchedDiagrams so the field is re-added to the level and survives mergeLevels without manual intervention - Preserves the existing warning for the case where neither source has the file Signed-off-by: Sinduri Guntupalli --- scripts/sync-adventure.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/sync-adventure.mjs b/scripts/sync-adventure.mjs index 29c1a11ed..b9e585366 100644 --- a/scripts/sync-adventure.mjs +++ b/scripts/sync-adventure.mjs @@ -231,6 +231,9 @@ async function main() { writeFileSync(resolve(diagramsDir, raw.architecture_diagram), svgContent); fetchedDiagrams.add(raw.architecture_diagram); console.log(` Fetched diagram: ${raw.architecture_diagram}`); + } else if (existsSync(resolve(diagramsDir, raw.architecture_diagram))) { + fetchedDiagrams.add(raw.architecture_diagram); + console.log(` Diagram not in challenges repo — using existing local file: ${raw.architecture_diagram}`); } else { console.warn(` Diagram not found at docs/diagrams/${raw.architecture_diagram} — add SVG manually to src/assets/diagrams/`); } From baa2cdfded1377458f5b7193b6af9a98d9dcf986 Mon Sep 17 00:00:00 2001 From: Sinduri Guntupalli Date: Tue, 21 Jul 2026 11:30:35 +0200 Subject: [PATCH 2/6] docs: audit and update ADVENTURES.md, CLAUDE.md for accuracy and completeness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADVENTURES.md: - Replace all em dashes with correct punctuation throughout - Expand pipeline diagram to show docs/diagrams/ → src/assets/diagrams/ path and list the other files the sync workflow stages (sitemap, prerender, tests) - Move YAML Templates section before the sync instructions so challenge authors find the field reference without reading through operational steps - Add missing fields to YAML templates: hook, scenario, estimated_time (level); meta_description, icon, rewards.eligibility, rewards.ranking_note (adventure) - Document that how_to_play step id is informational only and ignored by the generator and website - Fix rewards deadline code block: use correctly nested YAML, not dot notation - Rewrite checklist step 10 (llms.txt): the sync workflow modifies the file but does not stage it; reviewers must run npm run generate and commit manually - Add diagram_alt and meta_description to the preservation table - Correct architecture_diagram preservation note to mention both the challenges-repo fetch path and the local-file fallback - Add a link from the leaderboard step to the Refresh Scripts section - Number the PR checklist items to make ordering and dependencies explicit CLAUDE.md: - Add level.hook to the author-controlled prose fields list (was missing; the generator processes it identically to level.scenario which was listed) - Fix rewards.rankingNote to rewards.ranking_note (YAML key, not TypeScript camelCase) - Rewrite the "When adding a new adventure" checklist: the five stale manual steps are replaced with two accurate ones reflecting what the sync workflow now handles automatically Signed-off-by: Sinduri Guntupalli --- ADVENTURES.md | 356 ++++++++++++++++++++++++++++++++++++++++++++------ CLAUDE.md | 11 +- 2 files changed, 317 insertions(+), 50 deletions(-) diff --git a/ADVENTURES.md b/ADVENTURES.md index ac3d9c14c..c22d21343 100644 --- a/ADVENTURES.md +++ b/ADVENTURES.md @@ -1,8 +1,17 @@ # Adventures -This file is for anyone creating, syncing, or updating an adventure on offon.dev. +This file covers both sides of the adventure process: **challenge authors** working in the challenges repo and **website reviewers** completing the PR checklist after a sync. -Adventures live in a separate repo ([open-source-challenges](https://github.com/off-on-dev/open-source-challenges)) and are pulled into this site via the **Sync Adventure** GitHub Actions workflow. You never write the generated TypeScript files by hand — the workflow and build scripts do that automatically. +Adventures live in a separate repo ([open-source-challenges](https://github.com/off-on-dev/open-source-challenges)) and are pulled into this site via the **Sync Adventure** GitHub Actions workflow. You never write the generated TypeScript files by hand; the workflow and build scripts do that automatically. + +**Jump to:** + +- [YAML Templates](#yaml-templates): full field reference for challenge authors +- [Syncing a New Adventure](#syncing-a-new-adventure): trigger the workflow +- [Completing the PR Checklist](#completing-the-pr-checklist): what to do after the sync +- [Architecture Diagrams](#architecture-diagrams): SVG, ASCII art, and prose fields +- [Re-syncing an Open PR](#re-syncing-an-open-pr): updating an in-progress PR +- [Adding a Solution Walkthrough](#adding-a-solution-walkthrough): post-challenge write-ups --- @@ -15,13 +24,213 @@ off-on-dev/open-source-challenges offon.dev website repo beginner.yaml src/data/adventures//-posts.json intermediate.yaml ──── npm run generate (prebuild) ──► src/data/adventures/.generated.ts ... src/data/adventures/index.ts - src/data/adventures/summaries.ts + diagrams/ src/data/adventures/summaries.ts + -.svg ─────────────────────────────────► src/assets/diagrams/-.svg ``` +The sync workflow also regenerates `public/sitemap.xml`, `react-router.config.ts`, `e2e/smoke.spec.ts`, `src/test/seo.test.ts`, `src/test/prerender.test.ts`, and `scripts/refresh-leaderboard.mjs`. All of these appear in the PR diff; they are managed automatically and should not be edited by hand. + The generated TypeScript files are committed so the dev server works without running the generator manually. Never edit `*.generated.ts`, `index.ts`, or `summaries.ts` by hand. --- +## YAML Templates + +Full field reference for challenge authors. All fields are shown with example values. Remove any that do not apply to your adventure. + +### `docs/index.yaml` (adventure-level metadata) + +```yaml +# Title of the adventure. Use `title` (preferred) or `name`. +title: "My Adventure Title" +emoji: 🚀 + +# Optional: Lucide icon name to use instead of the emoji icon. +# Accepts any valid Lucide icon name (e.g. "Shield", "Cpu", "GitBranch"). +# icon: Shield + +# Tags drive the tag-filter UI and default `topics` for each level. +# Use the canonical tool/platform names shown on the OffOn website. +tags: + - Kubernetes + - Argo CD + - Helm + +# Optional: overrides the auto-generated SEO meta description for the adventure page. +# Keep under 160 characters. +# meta_description: "Fix broken Kyverno policies to restore proper admission control." + +# One or more story paragraphs. Markdown is supported. +backstory: + - "Opening paragraph that sets the scene." + - "Second paragraph continuing the story." + +# Optional: an overview of the challenge shown before the story. +# Useful when backstory is long and reviewers need a quick summary. +overview: + - "Brief, direct summary of what the participant will fix or build." + +rewards: + # ISO 8601 or human-readable: "Tuesday, 1 July 2026 at 23:59 CET" + # Supported TZ abbreviations: CET (+01:00), CEST (+02:00), UTC, GMT + deadline: "2026-09-01T23:59:00+01:00" + tiers: + - label: 1st place + description: 50% voucher for a Linux Foundation certification + - label: Top 3 + description: Credly badge to showcase the achievement + # Optional: overrides the default eligibility text on the rewards card. + # eligibility: "Open to all registered participants who submit before the deadline." + # Optional: overrides the default ranking note on the rewards card. + # ranking_note: "Ranked by verification timestamp; ties broken by submission order." +``` + +--- + +### `docs/.yaml` (level content) + +One file per level: `beginner.yaml`, `intermediate.yaml`, `expert.yaml`. + +```yaml +# Required. Must match the filename: beginner | intermediate | expert +level: beginner +emoji: 🟢 # 🟢 beginner 🟡 intermediate 🔴 expert +title: "Level Title" + +# Devcontainer folder name in off-on-dev/open-source-challenges/.devcontainer/ +# The generator auto-corrects this if it finds an unambiguous match. +devcontainer: my-adventure_beginner + +# Optional: upgrade the default Codespace machine size. +# Only set this when the level genuinely needs more RAM or CPU. +codespaces_machine: 4core + +# Optional: estimated completion time shown as a pill on the level card. +estimated_time: "2-3 hours" + +# One sentence shown on the adventure card and the level sidebar. +summary: "Fix the broken X so that Y works end to end." + +# Who this level is for. Markdown, inline code, and are supported. +# Use ABBR for acronyms on first use. +audience: >- + Platform engineers, SREs, and developers + curious about X. No prior experience needed, but familiarity with basic + `kubectl` and YAML will help. + +# Optional: a short hook shown at the top of the level page, before the story. +# hook: "The cluster is on fire and the policies that should protect it are broken." + +# Optional: the in-world scenario framing the level's context. +# scenario: "You have been granted emergency access to the broken cluster." + +# Level-specific story paragraphs. Markdown is supported. +backstory: + - "What went wrong and why it matters." + - "What the participant's role is in fixing it." + +# Bullet-point acceptance criteria. Markdown is supported. +# Keep each item concrete and testable. Use **bold** to call out key terms. +objective: + - "All workloads **missing the `required-label`** are blocked at admission." + - "All verification checks pass." + +# What skills and concepts the participant will practise. +# Use [linked text](url) for official docs. Use `backticks` for tool names. +what_you_learn: + - "How [X](https://example.com/docs) works and why it matters." + - "How to use `kubectl` logs to trace a silent failure across tools." + +# Architecture explanation. Shown under the Architecture heading on the level page. +# Use an array; each item becomes a separate prose block. +architecture: + - "High-level description of the system the participant is working in." + - "Which files or resources they need to touch, and which to leave alone." + +# Optional: SVG architecture diagram. +# Place the SVG at docs/diagrams/-.svg in the challenges repo. +# The sync auto-fetches it. Name must match the filename exactly. +architecture_diagram: "my-adventure-beginner.svg" +diagram_alt: "Left-to-right diagram showing how X connects to Y and Z." + +# Optional: ASCII art fallback when no SVG is available. +# Use a YAML block scalar (|) to preserve whitespace and line breaks. +# architecture_ascii: | +# ┌──────────┐ ┌──────────┐ +# │ Client │──────►│ API │ +# └──────────┘ └──────────┘ + +# Tools the participant will use. Shown as a toolbox on the level page. +toolbox: + - name: Tool Name + url: https://example.com/docs + description: "What it does in this challenge and how to open it." + +# Optional: running services exposed on local ports (Codespace / devcontainer). +# Omit if there are no local services. +services: + - name: My Service + port: 8080 + credentials: admin / password # omit if no login required + description: "What this service is and what to look for in it." + +# Step-by-step guide shown in the How to Play tab. +# Markdown, inline code, , and fenced code blocks are supported in both +# `title` and `content`. The `id` field is informational only; it is not used +# by the generator or the website. +how_to_play: + - id: start + title: "Start the Environment" + content: | + Start the platform with `make start`. The first run may take ~30-60 seconds + to pull images. Once it's up, leave it running in that terminal. + - id: explore + title: "Explore the Setup" + content: | + Open the CLI and inspect the + running resources: + + ```bash + kubectl get pods -A + ``` + + Look at what is deployed and note anything that looks broken or missing. + - id: fix + title: "Fix It" + content: | + The bug lives in `path/to/file.yaml`. Edit it directly and re-apply: + + ```bash + kubectl apply -f path/to/file.yaml + ``` + + When you think it's fixed, run the verification script: + + ```bash + make verify + ``` + +# Optional: further reading shown at the bottom of the level page. +helpful_links: + - title: "Official Docs: Feature Name" + url: https://example.com/docs/feature + description: "One sentence on why this link is useful for this challenge." + +# Optional: refine the default topics (which default to all adventure tags). +# Only set this when the level uses a subset of the adventure's tools. +# topics: +# - Kubernetes +# - Argo CD + +# Optional: override the verification step shown at the end of How to Play. +# Omit to use the standard verify.sh description. +# verification: +# command: make verify +# description: "What the script checks and what a passing result looks like." +``` + +--- + ## Syncing a New Adventure ### 1. Trigger the workflow @@ -30,25 +239,25 @@ Go to **Actions → Sync Adventure from Challenges Repo → Run workflow**. | Input | Required | Description | | --- | --- | --- | -| `adventure_url` | Yes | GitHub URL of the adventure folder — any branch works. Main: `https://github.com/off-on-dev/open-source-challenges/tree/main/adventures/05-lex-imperfecta`. PR branch: `https://github.com/off-on-dev/open-source-challenges/tree/feat/my-branch/adventures/05-lex-imperfecta`. | +| `adventure_url` | Yes | GitHub URL of the adventure folder. Any branch works. Main: `https://github.com/off-on-dev/open-source-challenges/tree/main/adventures/05-lex-imperfecta`. PR branch: `https://github.com/off-on-dev/open-source-challenges/tree/feat/my-branch/adventures/05-lex-imperfecta`. | | `levels` | No | Comma-separated level IDs to make live now (e.g. `beginner` or `beginner,intermediate`). Levels that exist in the challenges repo but are not listed here appear as "Coming Soon" placeholders. Leave blank to make all levels live. | ### 2. What the workflow does 1. Validates the URL points to `off-on-dev/open-source-challenges`. 2. If a PR branch (`feat/adventure-`) already exists, restores `adventure.yaml` from that branch so any manual edits already made survive the re-sync. -3. Fetches `docs/index.yaml` and all level YAMLs from the challenges repo. +3. Fetches `docs/index.yaml` and all level YAMLs from the challenges repo. For any level with `architecture_diagram` set, auto-fetches the SVG from `docs/diagrams/` and writes it to `src/assets/diagrams/`. 4. Writes `src/data/adventures//adventure.yaml` and creates `-posts.json` stubs for each new live level. -5. Runs `generate-adventures.mjs` to regenerate all TypeScript, sitemap entries, prerender entries, test arrays, `public/llms.txt`, and the leaderboard adventure list in `scripts/refresh-leaderboard.mjs`. +5. Runs `generate-adventures.mjs` to regenerate TypeScript, sitemap entries, prerender entries, and test arrays. 6. Opens (or updates) a PR on `feat/adventure-` with a checklist of steps to complete before merging. --- ## Completing the PR Checklist -The PR body lists everything that needs to happen before merging. Here is each item explained. +The PR body lists everything that needs to happen before merging. Complete the items in order; some steps depend on earlier ones. -### Add contributor block +### 1. Add contributor block ```yaml contributor: @@ -57,40 +266,45 @@ contributor: about: "One sentence bio." ``` -Add this to `src/data/adventures//adventure.yaml`. The `url` and `about` fields are optional but recommended. Once set, this block survives future re-syncs automatically. +Add this to `src/data/adventures//adventure.yaml`. The `url` and `about` fields are optional but recommended. This block survives all future re-syncs once set. -### Confirm month +### 2. Confirm month -The `month:` field defaults to the current month when first synced. Correct it if the adventure is planned for a future release. Format: `MMM YYYY` (e.g. `JAN 2026`). This field also survives re-syncs once set. +The `month:` field defaults to the current month when first synced. Correct it if the adventure is planned for a future release. Format: `MMM YYYY` (e.g. `JAN 2026`). This field survives all future re-syncs once set. -### Set community_category_id +### 3. Set community_category_id 1. Look up the Discourse category at `https://community.offon.dev/categories.json`. 2. Find the category for this adventure and copy its `id` integer. 3. Add `community_category_id: ` to `adventure.yaml`. 4. Run `npm run generate` to regenerate TypeScript. -This field also survives future re-syncs once set. +This field survives all future re-syncs once set. -### Update rewards deadline +### 4. Update rewards deadline -Change `rewards.deadline:` from `TODO` to either an ISO 8601 datetime or the human-readable format used in the challenges repo: +Change `rewards.deadline` from `TODO` to an ISO 8601 datetime or the human-readable format accepted by the generator: ```yaml -# ISO 8601 (preferred for direct edits) -rewards.deadline: "2026-07-01T23:59:00+01:00" +rewards: + # ISO 8601 (preferred) + deadline: "2026-07-01T23:59:00+01:00" -# Human-readable (accepted; the generator converts it automatically) -rewards.deadline: "Tuesday, 1 July 2026 at 23:59 CET" + # Human-readable (the generator converts it automatically) + # deadline: "Tuesday, 1 July 2026 at 23:59 CET" ``` Supported timezone abbreviations: `CET` (+01:00), `CEST` (+02:00), `UTC` (+00:00), `GMT` (+00:00). Unrecognised abbreviations are left as-is and logged as warnings during generation. -### Review topics +### 5. Review topics -Each level's `topics:` list defaults to all adventure tags. Refine it to the subset of technologies that are actually used in that level. This list is preserved on re-sync only if the challenges repo did not set it explicitly (see Re-syncing below). +Each level's `topics:` list defaults to all adventure tags. Refine it to the subset of technologies actually used in that specific level. The challenges repo value wins on re-sync when set explicitly there; a manually refined value in `adventure.yaml` is only preserved when the challenges repo leaves `topics:` unset. See [What is preserved on re-sync](#what-is-preserved-on-re-sync) for the full rules. -### Update discussion_url +### 6. Check architecture diagrams + +If the challenge author added an SVG to `docs/diagrams/` in the challenges repo, the sync fetches it automatically and no action is needed. If the sync log shows a warning that a diagram was not found, see [Architecture Diagrams](#architecture-diagrams) for the fallback steps. + +### 7. Update discussion_url Once you have created the Discourse thread for a level, use the **Add Discussion URL to Level** workflow (Actions tab → Add Discussion URL to Level → Run workflow). @@ -102,26 +316,17 @@ Once you have created the Discourse thread for a level, use the **Add Discussion The workflow updates `discussion_url` in `adventure.yaml`, fetches the initial posts from Discourse, regenerates TypeScript, and opens a PR. Run it once per level. If the thread is brand-new and has no posts yet, the PR will contain an empty `discussionPosts` array; the hourly `refresh-community-data` workflow will populate it once posts appear. -`discussion_url` in `adventure.yaml` is a website-only field. It is never in the challenges repo and survives every re-sync automatically. - -### Add architecture diagrams (if needed) +`discussion_url` is a website-only field. It is never in the challenges repo and survives every re-sync automatically. -If a level has an SVG architecture diagram, the sync strips the `architecture_diagram:` field because the SVG file must be added to `src/assets/diagrams/` manually. - -1. Add the SVG file to `src/assets/diagrams/.svg`. -2. Add `architecture_diagram: .svg` back to the level in `adventure.yaml`. - -Once set, `architecture_diagram` survives future re-syncs automatically. - -### Run the leaderboard script +### 8. Run the leaderboard script ```sh node scripts/refresh-leaderboard.mjs ``` -Run this after `community_category_id` is set. It adds the adventure to the leaderboard data used on the site. Requires `DISCOURSE_API_KEY` and `DISCOURSE_API_USERNAME` in your environment or a `.env` file. +Run this after `community_category_id` is set. It adds the adventure to the leaderboard data used on the site. See [Refresh Scripts](#refresh-scripts) for credential setup. -### Verify devcontainer paths +### 9. Verify devcontainer paths `generate-adventures.mjs` cross-checks each level's `devcontainer:` value against the actual folder names in [`off-on-dev/open-source-challenges/.devcontainer`](https://github.com/off-on-dev/open-source-challenges/tree/main/.devcontainer) via `gh api`. @@ -137,11 +342,19 @@ If you see this warning, also fix the `devcontainer:` value upstream in the chal If `gh` is unavailable or unauthenticated, the check is skipped with a warning and generation proceeds. -### Verify llms.txt +### 10. Update llms.txt + +`generate-adventures.mjs` patches `public/llms.txt` automatically, but the sync workflow does not commit that file. Run the generator locally and commit the result: + +```sh +npm run generate +git add public/llms.txt +git commit -s -m "chore: update llms.txt for " +``` -`generate-adventures.mjs` automatically patches the adventure entry in `public/llms.txt`. After running `npm run generate`, confirm the adventure appears correctly in the file under the Adventures section with the right title and URL. +Confirm the adventure appears under the Adventures section in `public/llms.txt` with the correct title and URL before pushing. -### Run the a11y audit +### 11. Run the a11y audit After the build passes, run the accessibility audit against any new or changed pages: @@ -151,7 +364,7 @@ After the build passes, run the accessibility audit against any new or changed p Target any new adventure or level detail pages. All severity-weighted findings must be resolved before merging. -### Final checks +### 12. Final checks ```sh npm run lint && npm run lint:reuse && npm test && npm run build && npm run test:e2e @@ -161,9 +374,64 @@ All checks must pass before merging. --- +## Architecture Diagrams + +Each level can display an SVG diagram, an ASCII art fallback, and one or more prose paragraphs. All are rendered under the **Architecture** heading on the challenge page. + +| Field | Type | Renders as | +| --- | --- | --- | +| `architecture_diagram` | SVG filename | `` (takes priority over `architecture_ascii`) | +| `diagram_alt` | string | Accessible alt text for the SVG. Required when `architecture_diagram` is set. | +| `architecture_ascii` | YAML block scalar (`\|`) | `
` block, shown when no SVG is present |
+| `architecture` | array of Markdown strings | Prose paragraphs always rendered below the diagram or ASCII block |
+
+### SVG in the challenges repo (normal path)
+
+Add the SVG to the challenges repo at:
+
+```text
+adventures//docs/diagrams/-.svg
+```
+
+Name it after the adventure slug and level: `dead-reckoning-intermediate.svg`, `lex-imperfecta-beginner.svg`. Then add the fields to the level YAML in the challenges repo:
+
+```yaml
+architecture_diagram: "dead-reckoning-intermediate.svg"
+diagram_alt: "One sentence describing what the diagram shows."
+architecture:
+  - "Prose paragraph explaining the architecture."
+  - "Second paragraph if needed."
+```
+
+The sync auto-fetches the SVG from `docs/diagrams/` and writes it to `src/assets/diagrams/`. No action is needed on the website side.
+
+### SVG already in the website repo (fallback)
+
+If the SVG exists in `src/assets/diagrams/` on the website repo but not in the challenges repo (added manually before the auto-fetch path existed), the sync recognises it and re-adds `architecture_diagram` to the level automatically. Check that `architecture_diagram` and `diagram_alt` are set for that level in `adventure.yaml`:
+
+```yaml
+architecture_diagram: "-.svg"
+diagram_alt: "One sentence describing what the diagram shows."
+```
+
+### ASCII art fallback
+
+When no SVG is available, use `architecture_ascii` with a YAML block scalar to preserve whitespace:
+
+```yaml
+architecture_ascii: |
+  ┌──────────┐       ┌──────────┐       ┌──────────┐
+  │  Client  │──────►│   API    │──────►│    DB    │
+  └──────────┘       └──────────┘       └──────────┘
+```
+
+All architecture fields survive every re-sync once set.
+
+---
+
 ## Re-syncing an Open PR
 
-If the challenges repo is updated while your PR is still open, or you want to promote a "Coming Soon" level to live, just run the workflow again with the same (or updated) inputs. You do not need to close or recreate the PR.
+If the challenges repo is updated while your PR is still open, or you want to promote a "Coming Soon" level to live, run the workflow again with the same (or updated) inputs. You do not need to close or recreate the PR.
 
 ### What happens
 
@@ -180,10 +448,12 @@ If the challenges repo is updated while your PR is still open, or you want to pr
 | --- | --- | --- |
 | `contributor:` (adventure) | Always | Survives every re-sync once set |
 | `community_category_id:` (adventure) | Always | Survives every re-sync once set; position is kept directly after `slug` |
+| `meta_description:` (adventure) | Always | Survives every re-sync once set |
 | `month:` (adventure) | Always | Survives every re-sync once set |
 | `discussion_url:` / `community_url:` (level) | Always | Website-only fields; never in the challenges repo. Both field aliases are preserved independently |
-| `architecture_diagram:` (level) | Always | Stripped from incoming; preserved once added manually |
-| `topics:` (level) | Only if challenges repo did not set them | If the challenges repo sets `topics:` explicitly, the upstream value wins |
+| `architecture_diagram:` (level) | Always | Auto-fetched from `docs/diagrams/` when present in the challenges repo; otherwise recognised from `src/assets/diagrams/` if the file exists locally |
+| `diagram_alt:` (level) | When upstream omits it | If the challenges repo sets `diagram_alt:` explicitly, the upstream value wins |
+| `topics:` (level) | When upstream omits them | If the challenges repo sets `topics:` explicitly, the upstream value wins |
 | `upcoming_levels:` entries for levels not yet upstream | Always | Placeholders for levels not yet authored in the challenges repo survive re-syncs so "Coming Soon" cards are not dropped |
 | All other level content | Never | Steps, objectives, toolbox, services, how_to_play, verification, etc. are always refreshed from the challenges repo |
 
@@ -214,7 +484,7 @@ The fastest way to add a solution is with the Claude Code skill:
 /add-solution
 ```
 
-Paste or attach the walkthrough content in any format — markdown, YAML, HTML, or plain text. The skill infers the adventure ID, level ID, and contributor name from the content where possible, confirms them with you, and then:
+Paste or attach the walkthrough content in any format: markdown, YAML, HTML, or plain text. The skill infers the adventure ID, level ID, and contributor name from the content where possible, confirms them with you, and then:
 
 1. Parses the input into structured steps (`SolutionBlock[]` arrays with text, code, image, and callout blocks).
 2. Downloads any referenced images and converts them to WebP at quality 85 using `cwebp`. Images are saved to `public/solutions//`.
diff --git a/CLAUDE.md b/CLAUDE.md
index a43136381..0d57dfd05 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -242,7 +242,7 @@ When diagnosing a bug, especially in the production build, follow these rules wi
 - **Buttons:** use raw `