Cheldrop Docs

Cheldrop Polish Plan — 15 August 2026

Companion to docs/cheldrop-build-plan.md (v2, 2 August). This plan collects the polish and correctness work identified by Chris after using the current build with the folder architecture, live sync, and Cloudflare sync in place. Same working method as the build plan: each item is a mini-brief for a fresh Claude Code session in ~/Developer/Cheldrop/app. Read the functional spec's "What must not break" section before touching anything. One item per session, test manually, npm run build:mac + npm run verify:mac when main-process code changed, commit.

Ordering logic: Part A is correctness — bugs and diagnoses that erode trust in the product and must be understood before more features stack on top of them. Part B is the sites list, cheap and visible. Part C is the publish/creation flow. Part D is TOC ambitions. Suggested session order at the end.


Part A — Correctness first (diagnose, then fix)

A1 License amnesia on every update — CRITICAL before auto-update ships widely — DONE 16 August

Diagnosis (verified against Chris's own key, not guessed): none of the four hypotheses was right; it was a variant of #4. validateLicense() checked data.meta?.store !== 'cheldrop'. Lemon Squeezy's /licenses/validate response has no meta.store field at all — it returns meta.store_id: 319053 (numeric) and meta.variant_id. So the comparison was undefined !== 'cheldrop', which is always true: every launch validation failed, fell through to the catch-all, and set licenseStatus = 'invalid'. It read as "the update logged me out" only because Cheldrop runs continuously in the menu bar, so an update is in practice the only time it relaunches. Chris's key showed activation_usage: 25 — 25 manual re-activations. VERSION_CUTOFF was not the cause (his key is dated 2026-03-22, one day past the cutoff) but was a live landmine and has been removed from the validation path anyway.

Fixed: cutoff gates activation only; validation now branches on a real reason (offline / server_error / unknown_instance / expired / disabled / wrong_product / rejected) and only a definitive rejection revokes. A missing instance re-activates silently. Installs already broken by the old bug heal themselves on the next launch. Failures are recorded locally in licenseLastFailure (config.json, never transmitted) and surfaced in Settings with a specific message per reason. Regression tests: npm run test:license (11 cases, no network).

Symptom: After installing a new version, the license key is still present in Settings, but the license is no longer active. Chris must re-activate manually every time.

Why critical: Auto-update (build plan Phase 2) means every user gets "logged out" of their paid features on every release. That reads as a broken product and generates support mail. This must be fixed and regression-tested before the next release goes to anyone else.

Where to look: main/license.js (activateLicense, validateStoredLicense, VERSION_CUTOFF), main/ipc.js (get-license-status, the 7-day offline grace logic, any settings-defaults merge in get-settings), electron-store keys licenseKey, licenseInstanceId, licenseStatus, licenseLastChecked, machineId.

Hypotheses, in order of likelihood — verify, don't guess:

  1. VERSION_CUTOFF (currently '2026-03-21') is checked on validation, not just activation. If the stored key's purchase date predates the cutoff, every fresh launch validation could downgrade the status even though the key was legitimately activated. Check what Chris's own key's purchase date is and what the cutoff check actually compares. If this is the cause, the fix is to make the cutoff gate new activations of the current version, not silently invalidate an already-activated install — or to compare against the version the key is entitled to, properly.
  2. Something in the update/install path resets licenseStatus while leaving licenseKey — e.g. a defaults merge in get-settings/save-settings that overwrites unknown keys, or the status being stored inside a settings object that gets rebuilt on first launch of a new version.
  3. machineId regeneration: if the store key name or userData path changed between builds, the fingerprint changes and Lemon Squeezy validation fails for the stored licenseInstanceId. (The key being visible argues the store survived, but check that licenseInstanceId and machineId also did.)
  4. validateStoredLicense() mishandles a specific Lemon Squeezy response (e.g. status: "inactive" for the instance) and clears the activation instead of re-activating or surfacing it.

Fix requirements: An installed, activated license survives version upgrades with zero user action. If validation genuinely fails (refunded, disabled), tell the user why in Settings, don't just silently drop to free. Log the actual LS response on validation failure (locally, not telemetry) so the next occurrence is diagnosable.

Done when: Install vN+1 over an activated vN → license still active on first launch, no re-entry. Same test offline → active via grace period. validateStoredLicense() still never blocks window creation.

A2 Cloudflare sync: what does it actually scan, and why are sites missing? — DONE 16 August

What it scanned, precisely (measured against Chris's own account, not guessed): three read-only listers per account, where the accounts are those derived from settings.zones — 2 of the 3 the token can see. The third (Laytr@nextpage.no's Account) holds no zones and, checked directly, no Workers, no Pages projects and no buckets: an account with no zone cannot host a Cheldrop site, so that derivation is complete by construction. Scope was never the problem. Three other things were.

Cause 1 — Pages pagination, and it was the big one. listPagesProjects sent no parameters, on the recorded belief that the endpoint returns everything in one response. It does not: it returns ten, always, and reports result_info.total_pages. Chris's account has 19 projects across 2 pages. The scan saw page 1. Of his 11 chel- Pages projects, 3 were visible and 8 were invisible — folder-test, fruit, test, pets, files-test and ls-test on utkast.com, plus www.cluewalks.com and www.cheldrop.com. per_page cannot be used to widen the page: the endpoint 400s on it at any value. The loop now follows total_pages with page= alone.

Cause 2 — one R2-less account blinded R2 everywhere. The ellaswool.com account has never enabled R2, so it answers 403 with code 10042, "Please enable R2 through the Cloudflare Dashboard". The lister read 403 as "cannot look", which correctly (by the Phase 6 rule) marked the whole R2 kind known: false — across both accounts. Effect: no R2 site was ever checked and no R2 orphan was ever offered. But 10042 is an answer, not a silence: that account has zero buckets. It is now read as an authoritative empty list, and the poisoning rule still applies to real permission failures.

Cause 3 — sites offered as imports of themselves. Orphan claiming was per kind:name. A Workers site that used to be R2 still owns its bucket, so chel-brand-papaya-no and chel-downloads-cheldrop-com showed up as orphans of sites already in the list — buttons whose only possible outcome was "Cheldrop already knows about that site". Claiming is now by resource name across all kinds.

Pagination was checked on the other two listers as well: Workers returns every script in one response and sends no result_info at all (it accepts and ignores per_page/page), and the R2 cursor loop was already correct.

Naming derivation needed no change — all 11 chel- resources reverse-map cleanly against the zone list, so nothing is unmappable today. The existing "on a domain this token can't see" listing covers the case if one ever appears; manual-domain import was not built for a case that does not exist.

One judgement call taken in-session: an orphan's backend now comes from backendForKinds()r2 requires a Worker and a bucket. chel-www-cheldrop-com exists as both a Pages project (serving www.cheldrop.com) and a leftover R2 bucket; the old rule preferred r2, which would have pointed a later delete at a Worker that does not exist and left the live Pages project untouched.

Result, measured: before, the scan found 3 of 11 chel- Pages projects, 0 R2 buckets and offered 0 imports. Now it reads 6 Workers, 5 buckets and 19 Pages projects across 40 domains in 2 accounts, and offers 8 real imports with no false ones. Sites shows that sentence under the check button.


Symptom: Chris has many live sites in Cloudflare, published with Cheldrop, that do not appear in the list after sync.

First deliverable of this session is an answer, not code: document precisely what the current implementation scans — which endpoints, which account(s)/zone(s), Pages projects and/or Worker scripts, what name filter, whether results are paginated. Then diagnose against Chris's account.

Likely causes to check:

  1. Pagination: Cloudflare list endpoints default to 20–50 per page. Many sites → anything past page 1 is invisible if page/per_page isn't handled.
  2. Only one resource type scanned: R2-backed sites are Worker scripts + buckets, Pages sites are Pages projects. If the scan only lists one of these, the other half of the fleet is missing.
  3. Scope: the scan may run only against the currently selected zone/account while sites span several zones. The join key (chel-{subdomain}-{domain-with-dashes}) encodes the domain, so a scan must cover every zone the token can see — or at least every zone in settings.zones.
  4. Naming-derivation mismatch on edge cases: multi-level subdomains, domains with dashes (ambiguous reverse mapping), old sites created before any derivation change. Do not "fix" this by varying the derivation — the spec forbids it; instead treat unmappable chel- resources as importable-with-manual-domain.

UI addition: after a sync, show a summary line — "Scanned N Pages projects and M Workers across Z zones" — so a user can see the scan happened and what it covered. Silent partial scans are how trust dies.

Done when: every chel- resource in Chris's account shows up (or is listed as unmappable with a reason), and the scan summary is visible in the app.

A3 Imported sites show as "Draft" — fix the status model — DONE 16 August

Root cause was as suspected, with one correction to the fix the brief proposed. siteStateLabel() read site.publishedAt ? 'Live' : 'Draft', and imported entries carry publishedAt: null — so every site Cheldrop found on Cloudflare read as a Draft. It also conflated two different things: a published site with no watcher was labelled "Live" alongside one that is genuinely being watched.

The brief's suggested fix — set publishedAt from the remote resource's metadata — would break the spec ("Imported entries keep publishedAt: null. Setting it would hand a free-tier user a second published site and let Live run on a folder that was never published"). importedAt carries that meaning instead, and the gates keep reading publishedAt, because they ask a different question. The Pages API does expose created_on, so the option is there if a date is ever wanted for display.

Built as the five-state model, decided once in siteStatus(): Needs you · Publishing… · Live · Published · Draft, with a distinct dot per state (green filled / accent hollow / accent pulsing / red / grey) and a tooltip saying what each means. Draft was kept — it has a genuine occupant: a folder adopted from disk that has never been pushed. The tray lists only Live folder sites, so it already agrees with the model. MCP's siteView.published had the same lie and now reads publishedAt || importedAt; full wiring of the model into list_sites stays deferred as the brief says.


Symptom: Sites discovered on Cloudflare get a "Draft" badge. Wrong on its face: if it exists on Cloudflare, it is published.

Root cause is presumably that imported registry-only entries are created without publishedAt (or with some status default), and the badge logic reads absence-of-publishedAt as "Draft".

Fix as a small status model, not a badge patch. Define the states once, render them consistently in the sites list, tray menu, and (later) MCP list_sites:

Imported entries get Published, with an affordance to adopt/link a local folder before "Rediger" can do anything meaningful.

Where: Sites.jsx badge logic, the import path in the Cloudflare-sync code (set a sensible publishedAt from the remote resource's metadata if available, or unknown handled explicitly), tray module if it echoes status.

Done when: no site that exists on Cloudflare ever displays "Draft"; each state above is visually distinct; A2's imported sites all read Published.


Part B — Sites list polish

B1 Row actions: two buttons + overflow menu — DONE 16 August (12fb0a1)

Built as specified, with two decisions taken in-session. Labels are English (Edit, View site ↗), not the Norwegian in the brief — the whole app UI is English and a two-language sites list would read as a bug. Say the word and they flip. For imported registry-only sites, Edit stays enabled and routes to Publish as usual; its tooltip says Cheldrop holds no copy of the files, so publishing from there replaces what is live. The adopt/link flow belongs to A3, and building half of it here would have meant building it twice.

Two inherited styling bugs surfaced and are fixed: .sites-list had overflow: hidden (it clipped the popover), and the actions column wrapped under long folder paths.

What: Each site row gets exactly two visible buttons and one overflow menu:

For imported registry-only sites (A3): Rediger routes to the adopt/link flow instead of Publish, or is disabled with a tooltip — decide in-session, but never a dead button.

Done when: all five actions reachable in two clicks or fewer; menu dismisses on outside click and Escape; nothing regressed in delete or open-external.

B2 Delete dialog: say what actually happens — DONE 16 August (12fb0a1)

One premise in this brief was already stale: the delete path no longer needs adminSecret. The R2 purge moved from the Worker's endpoint on the live domain to the R2 API (see the spec's "Nothing in publish or delete may call the live domain"), so deleteSite() never reads the secret. The honest-edge requirement was met against the failures that can actually happen instead: no API token, zone not in settings, and a bucket that could not be emptied — each said in words that name a next step and state that nothing was changed. A folder that would not move to the Trash is now reported at page level, since its row has already gone.

What: The delete confirmation must state explicitly: this removes the site from Cloudflare (Pages project or Worker + R2 bucket, plus the DNS record) — no files are deleted from your Mac. For folder sites, add the keep-or-trash-folder choice from build plan 4.1, defaulting to keep, with copy that makes "keep" the obviously safe default.

Also handle the R2 edge honestly: when adminSecret is missing, the current code refuses to delete — the dialog/error should explain what that means and what to do, not just fail.

Done when: a nervous user can read the dialog and know precisely what is destroyed and what survives, in both backends, in both modes.


Part C — Creation and publish flow

C1 Site folder is the default, offered before publish — DONE 16 August (906f798)

Built as specified. The site-folder decision moved from small print after the publish to a normal toggle inside the new Site settings group, above the Publish button, showing the exact destination path. A files-mode publish now materializes into ~/Documents/Cheldrops/<site-id>/ with no second click; opting out leaves the old one-click button in the result panel as the fallback, and the result panel says which folder the site now is, with Show in Finder. The global default is Settings → Site folders → "Save published sites as site folders", default on.

One judgement call taken in-session: a site that has already published as loose files defaults the toggle off, whatever the global setting says. The global default answers "what should a new site be"; silently converting an existing fire-and-forget site into a watched folder on its next re-publish is a different question, and not one a default should answer.


Symptom: Chris was asked (in small print, after publishing) whether to save files as a site folder. Wrong on all three counts: it should be the default, it should be a normal-weight control, and it should be decided before publish.

What: For drag-drop (Files mode) publishes, materializing into ~/Documents/Cheldrops/<site-id>/ becomes the default behavior. The publish form shows it as a regular, visible option before the publish button — e.g. a checked toggle "Lagre som site-mappe i Cheldrops" with the destination path shown — not a post-publish afterthought. A global setting in Settings ("Save published sites as site folders", default on) controls the toggle's default. Post-publish, confirm where the folder went with a "Vis i Finder" shortcut.

This aligns the flow with the architecture: the folder is the site; drag-drop is an on-ramp, not a parallel mode. Keep the opt-out because some publishes are genuinely fire-and-forget.

Done when: a fresh drag-drop publish lands in Cheldrops/ without the user doing anything; opting out is one visible click before publishing; the setting flips the default.

C2 New-site settings live at creation time — DONE 16 August (906f798)

Built as one visible "Site settings" group in Publish, above the publish button and behind no disclosure triangle: storage, site folder (C1), Live, table of contents, toolbar, password. Its header says where the settings are kept — this site's cheldrop.yaml for folder sites — and every one of them round-trips through that file, so changing them later in Rediger reads and writes the same place. C3's theme picker and D1's TOC mode drop into this group when they land; nothing else was built ahead of them.

Live defaults on, and is now settable before the first publish. It used to be a disabled toggle with "publish this site once first"; it is a setting of the site like any other, so it is chosen here and written to the yaml, and it starts working when the site publishes. The default is one function, sitefolder.defaultLiveFor(), consulted only where a folder is created — never in normalizeConfig, which would have turned a default into an override and switched Live on for every adopted folder that omits the key. A folder inside iCloud, Dropbox, Google Drive, OneDrive, Creative Cloud, pCloud or Sync.com is created with Live off, and turning it on there states the one-machine-per-site rule at the toggle. ~/Documents itself can be an iCloud folder (Desktop & Documents sync), so the Cheldrops root is tested, not assumed.

One consequence worth knowing: the launch-at-login offer used to fire from the Live toggle, and Live can now reach "on" without anyone touching it. It is called from the manual publish path and the materialize path as well — never from performPublish, which is also the unattended path, because a dialog belongs to a button press and not to a watcher firing at 3am. It is ask-once, so the extra call sites cost nothing.

Build plan updated in the same session: Phase 4.2's "default off" now records the 15 August decision and the cloud-path exception, and its "Live requires a publish first" note carries the amendment.


Symptom: Settings that belong to creating a site (folder behavior, live status, theme) are scattered, missing, or appear too late.

What: The creation/publish form gets a coherent settings group per site, all of which write to cheldrop.yaml for folder sites: backend, site folder (C1), Live on/off, theme choice (C3), TOC mode (D1), toolbar, password. Grouped and visible — not buried behind disclosure triangles the first time through.

DECIDED — 15 August (Chris): Live defaults on. This supersedes the 2 August default-off decision. The two defaults compose: new sites live in the Cheldrops root by default (C1), and sites in the Cheldrops root are Live by default — so the normal case is folder + Live with zero configuration. Adopted folders living inside iCloud/Dropbox paths (detect via path) default to Live off, with the one-machine-per-site rule surfaced at enable time, because that's where the publish ping-pong risk lives. The debounce and 1/60s rate cap from 4.2 are the safety net either way. Update docs/cheldrop-build-plan.md (Phase 4.2 says "default off") in the same session so the two documents don't contradict each other.

Done when: creating a site is one screen where every yaml-backed setting is visible and correctly defaulted; changing them later (Rediger) round-trips through the same UI to the same yaml.

C3 Starter themes actually exist — DONE 16 August

Built as the mechanism, with real CSS already in place. Three themes ship as files in templates/themes/ — Pond, Ember, Ultraviolet — alongside "Cheldrop default", which means no cheldrop.css at all. They are the blocks the Stillwater+ work had already designed and measured; templates/cheldrop-themes.css, which held them as copy-this-by-hand samples and was referenced by nothing, is gone. What the reskin still owes is a pass over these four looks, not a mechanism.

A theme is a file, and the file is the record. Picking one copies the stylesheet into the folder as cheldrop.css — an ordinary file to edit, rename or delete. Nothing about the choice is stored anywhere else: which theme a folder wears is read back out of the cheldrop-theme: line in the file's own header, so a folder carried to another Mac still knows, and a hand edit is detected by comparing the file with the bundled original. Adding a theme is dropping a file into templates/themes/; its name, blurb and preview swatch are parsed from the file, and the "Cheldrop default" swatch is parsed from md-wrapper.html, so no registry can go stale.

The one guard: a cheldrop.css that has been edited, or that never came from a starter theme, is never overwritten on the first click. The main process returns needsConfirm and the picker asks — including when the answer would delete the file (choosing the default look). The same guard runs on the create-folder and save-as-folder paths, which report the conflict rather than overwrite or fail silently.

One judgement call taken in-session: a publish-and-forget site has no folder to keep a stylesheet in, but it still gets the picker — the chosen theme travels with the publish and is injected identically, and the id is cached on the site entry so a re-publish keeps it. The alternative was to hide the control for half the publishes, which would have made the theme look like a folder feature rather than a site setting.

Where it landed: main/sitefolder.js (bundled themes, detection, apply/remove, the overwrite guard), main/ipc.js (three handlers plus the loose-files publish path), main/preload.js, Publish.jsx/Publish.css (the picker inside the C2 settings group), docs/cheldrop-skill/SKILL.md.


Symptom: The app looks like it promises default themes; none are visible anywhere.

What: Ship 2–3 bundled starter themes as cheldrop.css files (plus "Cheldrop default" = no file). Selectable at creation (C2) and changeable per site later. On selection, copy the theme file into the site folder as cheldrop.css — it must remain a plain, user/Claude-editable file per the architecture, not a reference into app internals. Changing theme later overwrites after a confirm (the file may have been hand-edited).

Sequencing: the themes are part of the new design language, so final versions belong with the reskin (build plan Phase 8 explicitly makes starter themes part of that deliverable — see the redesign brief). Build the mechanism (bundled themes dir, picker, copy-on-select) now with the current look as placeholder; swap the CSS when the reskin lands. Don't design twice.

Done when: picker shows the bundled themes with some form of preview; a chosen theme is injected into all generated pages exactly like any hand-written cheldrop.css.


Part D — TOC ambitions

D1 Sidebar TOC mode ("docs site" mode) — DONE 16 August

Built as specified. toc: index | sidebar | none in cheldrop.yaml, either as the bare mode or as a mapping with an items: list; show_toc is still written, always in agreement with the mode, so a folder written now still reads correctly in an older Cheldrop — schema_version did not move. toc: wins where the two disagree. An old caller sending show_toc: true cannot demote a sidebar back to an index; it means "on", so the file's mode stands.

Sidebar mode injects a navigation rail into everything Cheldrop generates — wrappers, subfolder indexes, the site map, the 404 — and into user HTML only under theme_user_html, the same rule the theme link follows. It lists pages (anything whose published URL is an HTML page) plus one entry per subfolder; the title links to /; the current page carries aria-current="page". Order is alphabetical unless items says otherwise, with a one-level group whose label links to the folder it names, and anything left out appended alphabetically — a partial list promotes rather than filters. Every colour is a theme token, so cheldrop.css restyles the rail with the page.

index mode is byte-identical to before: test/toc.test.js (npm run test:toc, 15 checks) renders both ways and compares. Publish's TOC toggle became a three-way picker in the site-settings group; the MCP create_site gained toc in place of show_toc (the old parameter is still accepted), and the SKILL documents the docs-site recipe.

Left for the backlog as planned: collapsible groups, sidebar search, prev/next links. D2 (header/footer chrome) followed the same day — see below.

Original plan

What: Extend TOC from one boolean to a mode: toc: index | sidebar | none in cheldrop.yaml (keep show_toc working — migrate true → index, false → none; bump handling, not schema_version, if the schema stays backward-readable).

sidebar renders every generated page with a navigation sidebar listing the site's pages — which turns a folder of Markdown files into a docs site with zero build tooling. That's a genuinely strong use case for Cheldrop ("your docs are a folder").

Defaults: alphabetical order, flat single level. Optional ordering/hierarchy via a toc: section in cheldrop.yaml (preferred over a second YAML file — one config file per folder is the architecture; a separate cheldrop.toc.yaml only if the section proves unwieldy in practice):

toc:
  mode: sidebar
  items:            # optional; unlisted files are appended alphabetically
    - intro.md
    - guide:        # a group with children = one hierarchy level
        - setup.md
        - usage.md

Where: main/renderer.js (wrapper generation — the sidebar goes in the wrapper, so it applies to .md/.txt wrappers and generated pages; injection into user HTML follows the theme_user_html-style opt-in rule), templates/md-wrapper.html or a new sidebar-wrapper partial, yaml schema. Sidebar must respect cheldrop.css so themes style it. Mind the spec rules: wrapper naming appends .html; fileMap must include anything new or the result panel breaks.

Keep scope sane: active-page highlighting yes; collapsible groups, search, prev/next links — backlog.

Done when: a folder of Markdown files + three lines of yaml publishes as a navigable docs site; with no yaml, sidebar mode still works alphabetically; index mode output is byte-identical to today's.

D2 Header/footer in the wrapper — DONE 16 August

Unparked the same day D1 landed, which is exactly the condition the paragraph below set: the wrapper-chrome pattern was proven, so D2 is that pattern applied a second time rather than a new mechanism.

Built to the shape the paragraph specified, and no wider. header: (title, logo, link) and footer: (text, links) in cheldrop.yaml, both absent by default and never written back as empty shells — a two-file drop cannot grow a masthead it did not ask for. A bare string is the short form for each (header: Acme Docs). Injection follows D1's rule exactly: every generated page, user HTML only under theme_user_html, every colour a theme token so cheldrop.css restyles the bands with the page.

The composition with D1 is where the work actually was: the bands go full-bleed except where a desktop sidebar would swallow them, in which case they span the content column instead; the masthead is injected after the rail so the document reads masthead → navigation → content on a screen reader and stacks that way on a phone; and a header logo is kept out of the sidebar listing, because it is chrome, not a page.

Nothing in the yaml is trusted, since a folder can be adopted from anywhere: link targets must be http(s), mailto, tel, or a path/anchor (javascript: and data: are dropped), a logo may not climb out of the folder, and it is only linked once found in the set actually being published — an ignore glob or a typo leaves the title standing rather than a broken image.

Deliberately not built: UI. This section specifies yaml config, so the Publish settings group is untouched. If header/footer earns form fields later, that is its own decision, not a side effect of this one. test/chrome.test.js (npm run test:chrome, 13 checks) covers the config reading, the security checks and the no-chrome-by-default property.

Original plan

Chris's instinct is right that this is for later. Park it in the backlog with a shape: optional header: / footer: config in cheldrop.yaml (site title, logo path, footer text/links) rendered into the wrapper, themable via cheldrop.css. It composes with D1 (sidebar + header = complete docs-site chrome) and with the brandable-output story (Phase 9 backlog: "brandable TOC/toolbar"). Do it after D1 has proven the wrapper-chrome pattern. No work now beyond this paragraph.


Suggested session order

  1. A1 License amnesia — trust-critical, blocks confident releasing of everything else through auto-update.
  2. B1 + B2 Sites list actions + delete dialog — done 16 August.
  3. A2 + A3 Cloudflare scan + status model — done 16 August.
  4. C1 + C2 Folder default + creation settings — done 16 August, build-plan doc update included.
  5. C3 Theme mechanism — done 16 August. The mechanism ships with the Stillwater+ themes already in it; the reskin revisits the CSS, not the plumbing.
  6. D1 Sidebar TOC — done 16 August, ahead of the Phase 5 beta, so beta users exercise the docs-site use case from the start.
  7. D2 Header/footer — done 16 August, straight after D1 proved the wrapper-chrome pattern.

Items 1–4 belong before the Phase 5 beta: they are exactly the kind of confusion/trust issues beta users would otherwise spend their goodwill reporting.

Decision log (15 August)

No open questions remain — every item in this plan is actionable as written.

August 19, 2026 Raw file Download