§6.2 corrected and amended, §6.1 given the Home decision, and a "Phase 5 as
landed" section added with the ✅ in §8.
**§6.2's table was in the wrong order, and the order is now load-bearing.** The
three news categories were listed Five on Friday / Newsletter / Screenshots; the
website's array has them Screenshots / Five on Friday / Newsletter. That did not
matter while the table was only a mapping, but a stored `order` is an index into
the site's nav, so a row the admin never moved takes its sort key from this list —
and a key from the wrong list scrambles a partially-overridden nav. The rows are
now numbered 0-15 and the order is called out as verbatim.
Also written into §6.2: the `feature` values are deliberately **not** mirrored
into the table (`APP_MENU` stays the app's own source of truth for gating), and
the "no `APP_MENU` row" rule covers **seven** entries rather than four — the
three news categories as well as the hub boards.
Two decisions the org lead settled before code, both recorded where the rule
lives rather than only in the phase notes:
- **The news categories are ignored for the drawer**, on the same rule as the hub
four. The mapping still exists for phase 6's added links, where a category tab
is a perfectly good destination because the admin named it by path. The
alternative — surfacing such a row only when overridden — keeps AC-1 but lets an
override introduce navigation after all, so it was rejected.
- **An admin may hide the Home row**, mirroring the website, where `/` is hideable
from the public header. Home stays the start destination and stays reachable by
back-press. The contrast that makes it safe is with `/admin/navigation`, which
has three guards because hiding it would strip the only way to undo an override;
nothing about a hidden Home row is unrecoverable.
"Phase 5 as landed" records the sort-key finding above as the thing the spec did
not settle, that an untouched instance gets `APP_MENU` back by **identity** so
AC-1's drawer claim is an `assertSame`, that the app's own rows are partitioned
off rather than sorted (and what that would mean for a future interleaved row),
that `group`/`section` are read and dropped, and why `Routes.NEWS_ROUTE` is
declared beside `Routes.NEWS` rather than replacing it — with the consequence that
`destination.route` now carries a query and is compared on `substringBefore('?')`.
Android-app: RunicGateway/Android-app#38.
Co-Authored-By: Claude <noreply@anthropic.com>
§5.6 gains the three decisions the drafted section left open — the top bar's
text fallback, the hero's fixed cropped band, and the accessibility split
between a decorative logo and a named one — each with the reasoning that
settled it, so none is re-litigated from scratch. Adds the 32dp/24dp logo
heights, the six-times-height width cap, and the note that the app takes no
fallback hero image where the website substitutes its own emblem.
"Phase 4 as landed" records how the empty-slot rule is enforced by layout
rather than by a conditional at each call site, why the blank check runs on
both sides of the resolver, and the honest limit of the phase's tests: the
drawing cannot be tested here at all, since the app has no Robolectric and no
androidTest source set, so only the decision of whether to draw is pure.
Phase 4 ticked in §8.
Co-Authored-By: Claude <noreply@anthropic.com>
§5.3 rewritten against what the build measured rather than what the plan
estimated, plus a "Phase 3 as landed" section and the ✅ in §8.
Three corrections to the drafted section:
- The APK estimate was wrong by more than 3×. "Roughly 1.5-2.5 MB" becomes a
measured before/after table: 5,031,411 B → 13,574,703 B, +8.15 MiB. The
section now records that Merriweather is 6.08 MiB of that on its own, and the
two cheaper options the org lead costed and declined, so the number is not
re-litigated from scratch later.
- Family lookup must be global, never scoped to the role's option list. Two of
the three presets publish a font their own role's dropdown does not offer,
because resolveThemeTokens copies preset tokens verbatim — the same bypass
§5.4 already records for --shadow-card.
- Italics mirror the website's four rather than being skipped, and every
bundled family supplies the four weights the scale asks for (with IM Fell
English's single weight and Cinzel's untouched 500/600/700 as the two
documented exceptions), because a family is not confined to the role its
dropdown lives in.
Android-app: RunicGateway/Android-app#TBD
Co-Authored-By: Claude <noreply@anthropic.com>
The radii-and-shadow phase, shipped as Android-app#35. Seven notes, of which
three are amendments to the spec rather than records of it.
Section 5.4's elevation map is not a no-op and now says so, with section 2 and
AC-1 amended to match. Material3's filled Card is Level0 and FeatureCard drew
none of the shadow its own KDoc claimed, so the app has been flat since M5 -
while the runic-gateway preset it was drawn from selects the "Default" shadow.
The org lead chose to apply the map as written rather than rebase it on the flat
baseline, because rebasing would have collapsed none/Soft/Default onto 0dp and
left only Deep doing anything on the phone. The radius half is untouched by this
and remains a provable no-op.
Section 5.4's exact-string match is corrected to a nearest-blur match. The
fantasy preset's own --shadow-card is not one of SHADOW_OPTIONS' four values,
because resolveThemeTokens copies a preset's tokens verbatim and they never pass
through the admin form's dropdown.
Section 5.2 gains the rule the pill needed: CircleShape is a percentage, so it
has no shipped dp for a ratio to scale and the resolved px is taken as dp below
the 500px floor. It is a literal because there is no app scale to preserve, not
as an exception to the ratio rule.
Also recorded: why all 24 Card( call sites became ShardCard( (Material takes
elevation as a default argument, not from the theme), that --radius-pill reaches
only StatusPill, and that Shapes - unlike ColorScheme - does implement equals,
so the structural no-op proof is one assertion rather than a reflection walk.
PLAN.md 9's M12 entry records phase 2 landed.
Co-Authored-By: Claude <noreply@anthropic.com>
The colors phase, shipped as Android-app#34. Seven notes, of which three are
departures the build forced rather than choices it made:
ColorScheme has no equals in material3 1.3.0, so AC-1's "full equality, not a
spot check" is a field-by-field compare by reflection over all 36 color roles,
against a verbatim copy of the pre-M12 scheme held in the test.
ShardPillFg is derived from --accent-bright rather than being a sixteenth
token, under the rule 5.1 already states for ShardOnCta.
An instance with an env accent and no theme_visual row loses its accented CTA
buttons, because brand.accent now seeds --accent alone. That is 5.1's "wrong
twice over" being corrected, and it is the one thing on screen that moves.
PLAN.md 9's M12 entry records phase 1 landed.
Co-Authored-By: Claude <noreply@anthropic.com>
Two records, one milestone.
Phase 7 (reading nav_admin / nav_player) is cancelled. Section 6.4 already
made the case against it and scheduled it last so the call could be taken on
its merits with the rest working: the two authenticated navs reach four app
rows between them, which does not pay for a new authenticated fetch, a
session-keyed cache and its teardown. The app therefore makes no authenticated
settings call at all, and the player and staff drawer rows keep their coded --
and so localized -- labels, which is the one thing given up. Section 4's locked
decision, section 3's endpoint row and section 5.5's lifecycle paragraph are
amended to match so the doc does not contradict itself, and section 8 gains
the standing "Phase 7, cancelled" note.
Phase 0 (the contract and the appearance store) landed as Android-app#33, and
its "as landed" notes record two departures from what section 8 specified:
- `theme` is modeled as a raw JsonElement rather than Map<String,String>?.
kotlinx fails the decode of the whole object on a value of an unexpected
kind, and `theme` shares its payload with `brand` and `push` -- so one odd
token would have blanked the branding and dropped the push relay URL, the
opposite of section 2. It is coerced field-by-field instead.
- A failed refresh keeps the last good appearance rather than falling back to
NONE. Section 5.5 said best-effort; the distinction it did not draw is that
a moment of no connectivity on resume must not repaint a themed shard back
to the shipped defaults.
Co-Authored-By: Claude <noreply@anthropic.com>
The website merged runtime admin theming, brand assets and nav overrides to
main (website#126 / docs#109). The app reads exactly one field of it --
brand.accent -- and renders a hardcoded APP_MENU, so an admin who re-skins the
site and restructures the header sees none of it on the phone.
Adds docs/android/THEMING_AND_NAV.md as the design of record for M12, and the
PLAN.md §9 entry that anchors it. Plan only: no app code, no backend work.
Everything consumed is already live on website/main.
The points that shaped it:
- The app's ui/theme/Color.kt palette is already, value for value, the
runic-gateway preset -- M5 was drawn from the same theme.css the preset was
later extracted from. So "an untouched instance is unchanged" carries over as
a testable ColorScheme equality assertion, not an approximation.
- Radii apply as a ratio against that baseline, not as literal dp. The app's
Shapes came from the M5 mockup and genuinely differ (medium 12dp vs
--radius-card 10px); a literal mapping would restyle the untouched app the
day this ships, and copying the app's scale into the server would be a second
source of truth.
- Fonts are bundled, not downloadable: the Play Store font provider makes a
de-Googled device fall back silently. Seven families join the bundled Cinzel.
- Nav overrides are keyed by website paths, so the app needs a path -> route
table -- the one new cross-repo coupling here. An override for a path the app
does not surface in its menu is ignored: a nav override may never introduce
navigation.
- The gates are untouched. MenuAccess and MenuEntry.feature still run after the
merge, so hidden:false cannot un-hide what a role or the shard's visibility
config withholds.
- Read the resolved theme/brand fields, never the raw theme_visual/brand_assets
rows that ride along in the same payload -- re-deriving a palette from them
would be a second resolveThemeTokens in Kotlin, guaranteed to drift.
Nine phases into a fresh edge in both repos, reaching main as one edge -> main
merge, the same shape the website side used. Phase 0 must change nothing on
screen. Phase 7 (the authenticated navs) is marked optional: nav_player reaches
two app rows and nav_admin two, which is a thin return for a new authenticated
fetch and its cache teardown.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgfKv5cz5pbY3dPeofSE5a
Records the capability asked for before the edge -> main cutover: dropdown
sections in the public header, with the coded entries organised into them and
admin-authored links alongside.
§7 is amended rather than quietly contradicted. It said the override layer
"cannot introduce a `to` that is not already in the hardcoded NAV array"; that
remains true of every CODED entry, and the restated constraint spells out what an
added link may be — a same-origin path, carrying no gate of its own, advertising
a route rather than granting one — plus why the property is structural: coded
entries live in a map keyed by routes the base array declares, and everything
that can name an arbitrary path lives in `links`, where the rule is applied.
§6.4 gains the { items, sections, links } wrapper, including the two properties
worth knowing: a bare map still reads as the items map, and a nav with no
sections still stores one.
§9 gains three acceptance criteria — the empty dropdown does not render, an added
link cannot leave the origin, and deleting a section returns its entries to the
top level rather than removing them.
"Phase 10 as landed" records why the Public tab needed its own tree editor, why
moving between containers stayed a dropdown rather than a cross-container drag,
why the menu opens on click and its trigger is not a link, and the palette-vs-full-nav
bug this surfaced in the phase 6-8 save path.
Co-Authored-By: Claude <noreply@anthropic.com>
Marks the nav wiring and the builder UI landed, and records the five places the
build differed from the design:
- The server had no way to store a nav row. The design scoped 6-8 as client
work, but updateSettings would have written a nav object as "[object Object]"
— a save that 200s and does nothing, for ever.
- The server deliberately cannot check that a `to` exists: the base NAV arrays
are client constants, and a server-side copy would be a second source of truth
for navigation. Shape is the server's question, membership the client's.
- `hidden: false` is accepted and never stored, so hiding stays subtractive.
- The nav editor cannot be hidden, enforced in three places.
- Orders are written only when something actually moved, compared against the
base restricted to the rows the editing admin can see.
Phase 9 (hue-carrying rgba literals + Parchment) is cancelled rather than
deferred. The finding that motivated it is kept as the record: those literals
carry a hue, so they are a rough edge in the three dark presets and not only a
blocker for a hypothetical light one.
Co-Authored-By: Claude <noreply@anthropic.com>
Records Phase 5 of THEMING_AND_NAV.md as landed and documents the new route
and the shell lifecycle in BACKEND_DESIGN.md.
Where the build differed from the design: the upload is one admin-only call
that writes the settings row too (rather than the generic staff upload plus a
PUT, which would leave unreferenced files and let editors change the site's
identity); brand_assets needed a validator of its own because these are the
only settings values written straight into HTML as URLs; the shell cache
carries a TTL as well as explicit invalidation because it is per process; and
the logo went into all six MoonDot surfaces rather than three.
Also notes what was deliberately left alone: the shell's title and description
still come from BRAND_NAME rather than the admin-set site_title, and fixing
that would change the served shell for instances with no brand_assets row —
which is exactly what the phase's acceptance criterion forbids.
Co-Authored-By: Claude <noreply@anthropic.com>
Records where the build diverged from the design and why.
- The presets do not live in theme.css as [data-theme] blocks. Section 6.2 is
marked superseded and a "Phases 3-4 as landed" section explains the inline
--accent precedence problem that forced server-side resolution.
- Section 4.5's accentInt fix is struck through: getPublic() never exposed
accentInt, and Discord embeds are colored by a separate process reading env,
so there was nothing per-request to recompute. Replaced with what was
actually done -- the bot fetching the effective accent.
- Phase 9 re-scoped. Applying Fantasy on a live instance showed the section 4.8
rgba literals carry a hue, not just a light/dark assumption, so the dark
presets need that promotion too.
- BACKEND_DESIGN.md: the new /settings/theme/options route, the `theme` block
on /public/settings, effective values in the brand block, theme_visual
validation on PUT /admin/settings, route count 225 -> 226.
- api-route-inventory.json regenerated from the manifest.
Co-Authored-By: Claude <noreply@anthropic.com>
Matches RunicGateway/website's phases 0-2 of THEMING_AND_NAV.md.
BACKEND_DESIGN.md:
- The new /settings router group and its one route, plus why it is a fifth
group rather than a route on an existing one.
- DELETE /admin/settings/:key in the admin route table, with the allowlist and
why reset deletes instead of writing.
- The five unseeded theming/nav keys under the settings schema: absence of the
row is the "use the default" state, values are TEXT so consumers parse, and
malformed reads as absent.
- Route count 215 -> 225.
THEMING_AND_NAV.md:
- Phases 0-2 marked landed, with an "as landed" section recording the three
things the design left open: where /settings/nav lives, where
parseJsonSetting lives, and the exact 23-declaration radius promotion.
- The nav merge util's ordering rules, settled by the implementation: an
untouched item keeps its index as its sort key, an explicit order wins a tie
against a coincidental index, equal explicit orders keep code order, and
`group` is honored only when it names an existing section.
- All four PR pairs target `edge`; the feature reaches `main` as one merge.
api-route-inventory.json: resynced from server/routes.manifest.json. Picks up
the two new routes plus eight that were already missing from the mirror since
the Protocol 3.0 cutover (shard clilocs, market, points).
Co-Authored-By: Claude <noreply@anthropic.com>
Corrects the design doc against the current codebase and locks the open
decisions, in the same shape as HERO_EDITOR.md (locked decisions ->
corrections to reality -> phased build).
Blocking gaps found in the design doc:
- no delete path exists for a settings row, which every "reset to
defaults" in the feature depends on
- editors/moderators/players have no endpoint to read their own nav
overrides (GET /admin/settings is admin-only)
- renderIndexHtml runs once at boot, not per request
- settings values are JSON strings, not objects
- getPublic().brand is a cross-repo contract the Android app and
Discord embeds theme from; new keys would silently bypass it
Locked: effective values resolved server-side into getPublic().brand;
radius + shadow tokens only (spacing/border cut); radius tokens seeded at
today's real values so the promotion is a no-op; three dark presets in v1
with Parchment deferred; 12-option font shortlist across 8 web families
in one request; PNG-only favicons.
Co-Authored-By: Claude <noreply@anthropic.com>
v0.1.0 shipped on 2026-08-07, so every doc that said "not released yet"
was wrong the moment the cutover merged.
- INSTALL.md: replace the pre-release status banner. The installer is the
path the guide leads with; Appendix A is reframed as supported-not-
deprecated, for hosts that cannot run the binary, operators who want to
place files themselves, and development from a working tree.
- PLAN.md: status is Shipped, both cutover gates recorded as met (incl.
the Windows 1053 handshake bug the real SCM run found), Phase 5 table
and 5.4 closed out.
- README.md: point anyone setting up a shard at INSTALL.md first.
- link/link-README.md: mark the pre-split snapshot as historical, so its
deploy.ps1 instructions stop reading as the setup path.
Co-Authored-By: Claude <noreply@anthropic.com>
PLAN.md §8 recorded that `sc create` against the plain console sidecar worked
and needed no change to `link`. The first Windows install disproved it: 1053,
"a timeout was reached (30000 milliseconds) while waiting for the service to
connect", with SERVICE_EXIT_CODE 0.
The premise was a false symmetry with systemd. systemd supervises any
foreground process; the Windows SCM supervises only one that calls
StartServiceCtrlDispatcher within ~30 seconds. Record the reversal and what it
costs: link gains a Windows service entry point, kept at the edges so the whole
sidecar stays shared and Cargo builds neither Windows crate for Linux.
INSTALL.md:
- Troubleshooting gains a 1053 row naming the real cause (a sidecar older than
v1.2.0) and the two tell-tales that distinguish it from a crash: exit code 0,
and a foreground run of the same binary working fine.
- The existing "stops immediately" row said the same wrong thing; it now covers
the genuine-crash case only, and points at the log file and journalctl.
- §3 and Appendix A4 document the service log, and A4 states the version floor.
- Fixes a literal 0x08 byte in the backups path row, which rendered as
`%ProgramData%\RunicGatewayackups\` — the backslash had been eaten.
Co-Authored-By: Claude <noreply@anthropic.com>
Two corrections to §7.1, both forced by the first compose run that ever
had a bundle to write (org lead, 2026-08-05).
The section said bundles are committed to `main` and that this "needs no
new branch-protection exception: release.yml's version-bump commit
already requires the CI user to be able to push to main". Both halves
were wrong. `main` is protected and declines the push, and release.yml
had never pushed anything: its bump step has never executed in any repo
carrying it, because an empty template expression written literally in
one of its comments makes the runner fail to build the step and skip it
without failing the job. The tags exist because Gitea's release API
creates one when it publishes. The assumption that a working push path
already existed had never been tested by anything.
Bundles now publish to a `bundles` branch at its root, which keeps every
property the original choice was for -- reviewable diff, git history of
the compat matrix, plain anonymous raw URLs, no credentials on the shard
host -- and needs no exception. The release workflows are tag-only for
the same reason, as servuo-plugins has always been: the version is still
written into Cargo.toml before building so a released binary
self-reports correctly, but is not committed back.
Also updated: the raw URLs in INSTALL.md Appendix A1 and in Phase 0.3's
as-built note.
Co-Authored-By: Claude <noreply@anthropic.com>
PLAN.md §5.4. The operator guide is the specification of the run, so
these are part of building the phase rather than a write-up after it.
- The download list gains runicgateway-installer-linux-aarch64, with
`uname -m` as the way to tell, and says plainly that there is no macOS
and no Windows-on-arm build: the shard dials the sidecar out on
loopback, so the two share a host, and no ServUO host is either.
- Appendix A3 names the arm64 sidecar asset for the by-hand path, and
points at the bundle from A1 for the version rather than the one
written in the example.
- §7's `update` says what a backup is, when one is taken and when one is
not, and that restoring is the operator's to do -- the guide already
promised their Bridge.cfg edits survive, and this is the same promise
for the .cs file they edited that gets overwritten by design.
- --no-backup joins the flags table; --purge's row and the uninstall
table now name backups alongside the config, the database and the
cached patch set.
- The paths tables and doctor's sample output gain the backups
directory and its row.
Co-Authored-By: Claude <noreply@anthropic.com>
Two things this section got wrong, both found by implementing it.
§5.3 said a backup is taken for an update and for an install over an
existing record, because "a first install overwrites nothing". That is
not true of a tree deployed by hand per INSTALL.md Appendix A2 -- the
path this project recommends while the binary is unreleased. There the
first install finds .cs files that differ, plans them as Change, and
overwrites them with no prior record anywhere to notice. The rule is
now the direct one: back up whenever the run is about to overwrite
something. A genuine first install onto a clean tree still writes
nothing, because there is nothing to copy.
Also records that sidecar.toml joins a backup rather than causing one
(nothing rewrites it, so triggering on it would leave a dated directory
after every no-op update), and that the directory is created lazily
with the manifest written last, so an interrupted run can neither be
mistaken for a backup nor evict a good one.
§5.2 gains what its cross-builds turned up: neither crate builds with
the arm64 compiler alone. gcc-aarch64-linux-gnu only recommends
libc6-dev-arm64-cross while both release workflows install with
--no-install-recommends, so the C in each crate -- bundled SQLite under
sqlx, ring under ureq's rustls -- fails on a missing libc header while
every Rust dependency compiles fine.
Co-Authored-By: Claude <noreply@anthropic.com>
Phase 5 was sketched as four items; two of them are dropped rather than
deferred, because what stops them is an ownership conflict that does not
improve with time (org lead, 2026-08-05).
- No .deb and no MSI (5.1). A .deb under link's release would own the
binary, the systemd unit and the service user -- the three things
service.rs writes, hardens and removes and install.json records, so
uninstall would leave a dpkg-installed-but-broken package and an
apt upgrade would make doctor report drift nobody caused. The
binary-only variant buys apt-managed upgrades of one file, which
update already does from a protocol-checked bundle. An MSI
contradicts "the installer does not install itself" and adds a second
uninstall path beside the verb that owns install.json, the cached
patch set and the ServUO-tree report.
- Linux aarch64 for both components (5.2), in the order the bundle CI
forces: it hard-fails on an unrecognized link asset name and asserts
the platform keys present, so the name is taught on main first, link
publishes, the key becomes required, and only then does the crate on
edge learn it. bundle.yml is never edited on edge, so the cutover
merge has nothing to conflict over.
- Backup before overwrite (5.3), scoped by what cannot be fetched
again: not the binary or the overlay files, and not the database
(store.rs is CREATE TABLE IF NOT EXISTS over shard state the sweeps
repopulate -- a cache with a schema), but an operator's edits to a
deployed .cs file, which Phase 1 overwrites by design, and
sidecar.toml, whose token the website already holds.
- The docs a first release invalidates (5.4), including the repo README
still announcing Phase 1 four phases later.
Also records that the Windows SCM smoke was attempted on 2026-08-05 and
stopped at its first check on an unelevated shell, so that half remains
entirely unexecuted.
Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 8818c06f1d)
Two decisions from the org lead, recorded in the design of record.
Phase 5 (packaging polish) now runs BEFORE the edge -> main cutover
rather than after it. The original order assumed the cutover would cut a
v1 and packaging would follow as a v1.x, but this phase changes the
release layout itself: shipping first would mean a first release that is
immediately superseded, and operators who downloaded a bare binary being
told to re-download a package. Deferring costs nothing — nothing is
published from `edge`, and INSTALL.md's Appendix A is the supported path
meanwhile.
The cutover therefore has two entry criteria, stated in the status
header and at Phase 5: packaging polish, and the Windows SCM half being
verified on a real host. The second is called out explicitly because
`sc create`, the virtual service account, the failure actions and the
token-file ACL have still never been executed anywhere — and running the
systemd half for real is precisely what turned up a bug no unit test
had. Nothing should be released while the only untested code is the half
that registers a service.
Co-Authored-By: Claude <noreply@anthropic.com>
Phase 2 shipped service registration that had never been executed: a
relocated test run deliberately skips it, `sc create` needs elevation,
and systemd needs a Linux host. It has now been run for real on a
privileged Debian 12 container with systemd as PID 1 — unit written and
enabled, service up as the unprivileged runicgateway user, sidecar.toml
600 and owned by it, database under /var/lib (so the UOLINK_DB_PATH pin
works), /health answering protocol 3, and uninstall taking the service,
unit, binary and account away while leaving the config, the database and
the whole ServUO tree alone.
That surfaced one bug only a real service host could show — user_created
was recorded per-run rather than as state, so an identical re-run
rewrote install.json and uninstall silently left behind the account the
installer had created (installer#8). Recorded here with the reason it is
invisible on Windows.
Also notes what is still unverified: the Windows SCM half, which needs an
elevated shell this machine's automation does not have.
Co-Authored-By: Claude <noreply@anthropic.com>
The installer crate now implements the whole command surface INSTALL.md
published before the binary existed, so this records what Phase 4 turned
out to be and corrects two places where the plan and the guide had drifted
apart.
PLAN.md
- Status header: Phases 1–4 are on `edge`; the edge → main cutover now
cuts a binary that does everything INSTALL.md describes, with Phase 5
being packaging polish rather than capability.
- A Phase 4 "as built" section: why `update` is the install pipeline in
a different mode rather than a second implementation, why it neither
reprints the token nor stays quiet about a protocol change, the tier's
scope under `update` (re-resolve what was applied, without re-asking;
name what is new), how `doctor` asks the binary the way the service
does, the exit-code rule and why a stopped shard is a ⚠ while a
running one that has not dialed in is a ✗.
- §5's uninstall table: the cached patch set and patches/originals/ move
from "removed" to "kept". The report that command prints tells the
operator to diff against those originals — deleting them made the
advice impossible to follow within one command's output. `--purge`
removes them.
INSTALL.md
- §2: exit codes stated (`doctor` and `uninstall` use 1 for a completed
run that found something wrong), `--patches` now applies to `update`,
`--yes` means yes on `uninstall`, `--purge` covers the patch cache.
- §7 doctor: the real row set, what ✓/⚠/✗ mean, that it writes nothing
and is safe to run with the shard up.
- §7 update: it updates the tree install.json names, needs the shard
stopped, does not reprint the token, calls out a protocol change, and
what it does and does not do with the patch tier.
- §7 uninstall: what survives, that edited files are flagged in the
listing, the confirmation's default, and where the report file lands.
Co-Authored-By: Claude <noreply@anthropic.com>