chore(release): cut edge over to main — the Rust screens and the app's words (v0.6.0) #59

Merged
whitlocktech merged 32 commits from edge into main 2026-10-23 02:09:14 -05:00
Member

This is the Android cutover. edge → main carries 20 commits (and their merges) since v0.5.0: the whole Rust leg plus M20. After it merges I tag v0.6.0, a minor bump for new features on a pre-1.0 app, and release.yml builds the signed APK and publishes the release.

What ships

Rust (Module-Rust's screens, gated on the rust capability)

  • the server list and one server's page: feed, leaderboard, online, wipes (phase 5, leg A)
  • the player's own Rust account: link code, linked Steam accounts, what they hold in game (M15)
  • coin balances and the backpack on that account screen (§11.2, §11.3)
  • a Rust notification on the phone (phase 11)
  • the map on a phone (M17), the next wipe (M18) and chat titles on the leaderboard (M19)
  • named NPC markers, bigger bosses and an event's progress lines (RunicNPC stage 8)
  • weapon names in the feed (Module-Rust#60; an older module gets the stripped prefab name)

Core

  • several Steam accounts on the account screen
  • M20 (#58): core screens say "community" / "this site", UO and Rust words stay on their own screens, guarded by StringVocabularyTest; Moderation and Support queue appear only where the UO module is installed

Fixes: who is online is said to be withheld, never "nobody"; a link's when and where; the emulator walk's findings; CI's setup-android package fix.

Verified

  • On edge at c41aff9 (identical to this head), ./gradlew testDebugUnitTest lintDebug assembleDebug passes: 758 unit tests, 0 failures. This PR's own pr-checks run gates the merge.
  • Walked on the s22_ultra emulator on 2026-10-23 against the UO demo and the Rust demo (details in #58). The Rust screens were walked in their own phases.

After the merge

  1. Tag v0.6.0 on the merge commit, which triggers release.yml.
  2. Check the release's APK and checksums.
  3. runicgateway.com: bump platform.json (Android-app v0.6.0, Module-Rust v0.5.1 once Module-Rust#61 releases) together with the retaken rust-feed / app-rust-server screenshots.
  • AI-assisted: Claude Code (Claude Opus 5.5).

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

This is the Android cutover. `edge` → `main` carries 20 commits (and their merges) since **v0.5.0**: the whole Rust leg plus M20. After it merges I tag **`v0.6.0`**, a minor bump for new features on a pre-1.0 app, and `release.yml` builds the signed APK and publishes the release. ## What ships **Rust (Module-Rust's screens, gated on the `rust` capability)** - the server list and one server's page: feed, leaderboard, online, wipes (phase 5, leg A) - the player's own Rust account: link code, linked Steam accounts, what they hold in game (M15) - coin balances and the backpack on that account screen (§11.2, §11.3) - a Rust notification on the phone (phase 11) - the map on a phone (M17), the next wipe (M18) and chat titles on the leaderboard (M19) - named NPC markers, bigger bosses and an event's progress lines (RunicNPC stage 8) - weapon names in the feed (Module-Rust#60; an older module gets the stripped prefab name) **Core** - several Steam accounts on the account screen - **M20** (#58): core screens say "community" / "this site", UO and Rust words stay on their own screens, guarded by `StringVocabularyTest`; Moderation and Support queue appear only where the UO module is installed **Fixes:** who is online is said to be withheld, never "nobody"; a link's when and where; the emulator walk's findings; CI's `setup-android` package fix. ## Verified - On `edge` at `c41aff9` (identical to this head), `./gradlew testDebugUnitTest lintDebug assembleDebug` passes: 758 unit tests, 0 failures. This PR's own `pr-checks` run gates the merge. - Walked on the s22_ultra emulator on 2026-10-23 against the UO demo and the Rust demo (details in #58). The Rust screens were walked in their own phases. ## After the merge 1. Tag `v0.6.0` on the merge commit, which triggers `release.yml`. 2. Check the release's APK and checksums. 3. runicgateway.com: bump `platform.json` (Android-app v0.6.0, Module-Rust v0.5.1 once Module-Rust#61 releases) together with the retaken `rust-feed` / `app-rust-server` screenshots. - [x] AI-assisted: Claude Code (Claude Opus 5.5). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 32 commits 2026-10-23 02:05:56 -05:00
The app's half of module-rust's read path — the leg R10 says trails the website
surface it consumes by one phase, so it is built against routes that exist.

Two screens, mirroring what phase 4 shipped: `/rust` is the server list (D12),
and one server is a single screen with four tabs (D13) rather than four
destinations. Both render entirely from the website's own tables, so the phase
criterion — a fleet that is entirely off still shows its maps, seeds, wipe
dates, killfeeds, leaderboards and last known presence — holds here for the same
reason it holds on the web.

What is new to the app rather than copied:

- **A poll that is not a load.** `PollWhileResumed` + `refreshInto` (D17): a
  refresh is invisible when it succeeds and KEEPS the rows when it fails. The
  app had one shape for a read — blank, ask, replace — which is right for opening
  a screen and would clear the killfeed three times a minute here. Gated on
  RESUMED, so a backgrounded app makes no requests at all and returning to it
  refreshes at once.
- **A second game module in the drawer.** `Capability.RUST`, gating one row. It
  deliberately does not gate on `servers`/`killfeed`/`leaderboard`/`presence`/
  `wipes`: those name surfaces, core flattens every module's capabilities into
  one list, and another module declaring `servers` would reveal these screens on
  a site with no Rust. Module-Rust#5 adds the identity string.
- **`/rust` in NavPaths**, so an admin's nav override or an added link opens
  natively instead of handing off to a browser (D19).
- **A live player count on the drawer row** (D19) — the phone's answer to D15's
  footer slot, in the same badge slot the inbox count uses, with the same
  screen-reader treatment. Zero renders nothing; a failed read keeps the last
  number; it never polls.

Two things carried across from the website's own page walk rather than
rediscovered: "last reported" reads `lastSeenAt` and never `updatedAt` (a failed
poll moves the second), and a feed row from another calendar day carries its
date, or a row from a past wipe reads as this afternoon.

The four navigation tests that moved did so because APP_MENU gained a row and
the website's nav number line gained an index; each now says which.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
fix(rust): what the emulator walk found
Some checks failed
PR Checks / android-build (pull_request) Failing after 2s
a6677d5bf9
Three things, none of which a unit test could have seen.

**The drawer's live count resolved once per process.** It was keyed on the
capability answer alone, so it was read at connect and never again — which is
not what "live" means on a row somebody opens the drawer to look at. It now
refreshes on resume, beside the inbox's unread badge and for the same reason:
coming back to the app is exactly when a stale number would be noticed. Still
never on a timer, still nothing at all on a site without the module.

**Every card's text sat flush against its edge.** `ShardCard` is the themed
`Card` and carries no padding of its own — each caller pads its own content, and
these four did not. On a phone the first glyph of each line read as clipped.

**A name touched its own kill count.** Five numeric columns beside an
equal-weight name column left "Brannock" and "50" reading as one field. The name
now takes a wider share and ellipsizes, and the ACTIVE SORT is marked on the
header rather than by tinting a column of numbers — the header is the control,
and tinting the values says "these are special" instead of "this is what the
table is ordered by".

Walked against the phase-4 rig: a core with the module installed, one live
server and one that has never reported. Both halves of the phase criterion hold
on a phone — the Rust site renders every panel with its server unreachable, and
the same app against the UO core shows its five shard rows and no Rust row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
fix(ci): stop setup-android installing a package Google has removed
Some checks failed
PR Checks / android-build (pull_request) Failing after 18m49s
daf483f514
**Unrelated to this PR's feature work**, and fixed here because it blocks
verifying it (org lead, 2026-09-17). PR #46 passed on this workflow yesterday;
every Android PR fails now.

`android-actions/setup-android@v3` is a floating tag and the action's `packages`
input defaults to `tools` — an obsolete package Google has since removed from the
SDK repository. So the step runs `sdkmanager tools`, gets `Warning: Failed to
find package 'tools'`, exits 1, and CI fails in **Set up Android SDK**, before a
line of this repo is compiled.

`packages: ''` turns that install off. It was always redundant here: the very
next step installs exactly what the build targets — `platform-tools`,
`platforms;android-35`, `build-tools;35.0.0` — precisely so the build never
depends on what some action decided to fetch.

Not addressed here, and worth its own decision: `@v3` is a floating major tag, so
the next upstream change can break CI the same way without warning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
chore(ci): re-run
All checks were successful
PR Checks / android-build (pull_request) Successful in 11m23s
4b22ab3756
Run 76 hung in `compileDebugKotlin` for thirteen minutes and was failed with no
error in its log, where the last good run finished that task in two. Nothing
about that reads as a compile error, and this repo's Gitea has no rerun
endpoint — so this empty commit is the re-run, to tell a transient runner
problem from a real one before bisecting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Reviewed-on: #47
R10's leg B: identity, and the half of R2 a player is allowed to see. Every
route this calls existed and answered before a line of Kotlin was written,
except the entitlement read, which is this phase's one website change.

**One drawer row under the player group, shaped like `CharactersScreen` one
game along**: the code card first, then what the code got them. A link is
fleet-wide, so it is not a tab under one server — a Steam account is one
person wherever they play, while stats are per server and per wipe.

**Gated on `rust`, not on `identity`.** The module declares a surface word
per feature, and D16's rule is that a capability answers one question — *is
the module there*. `MenuAccess.PLAYER` is `isPlayer || isStaff`, which is
right here: `/player/rust/*` is `requireAuth` with no role above it, and
staff play the game too.

**Two reads, neither blocking the other.** An entitlement is authored
against the website account, so it exists before a Steam id does — the
person who has just been given something and has not linked yet is exactly
the one who needs both halves at once, and a failure on either leaves the
other standing.

**The four refusals stay four pieces of advice.** 400 is a spent code, 409
is a Steam account another account holds (`/unlink` in game releases it),
429 is the limiter, and 503 is a server that could not be reached — where
the code is still good, so it may not say "get a new one". A player told
otherwise goes back to the same unreachable server for another code.

The app does no scope arithmetic: `*` never reaches a screen. Each entry
arrives with its servers already resolved and each marked, because a second
implementation of `inScope` is a second thing to keep true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMH6bw1jXMgbyF3ZWGEzSM
fix(rust): say when and where an account was linked, as the website's row does
Some checks failed
PR Checks / android-build (pull_request) Failing after 22m6s
a079bd481e
Found by the emulator walk, not by the suite: the app's row carried the
name and the Steam id and stopped there, while the website's says "linked
just now on rust-oxide". Neither fact is part of the identity — a link is
fleet-wide — but which server minted the code is where a support
conversation starts, and the module's own schema comment says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMH6bw1jXMgbyF3ZWGEzSM
Reviewed-on: #48
fix(rust): say who is online is withheld, never that nobody is
Some checks failed
PR Checks / android-build (pull_request) Failing after 17m40s
f695eb6c45
Module-Rust now withholds who is online below an operator-chosen
audience - staff by default (org lead, 2026-09-22: nothing names who is
online by default). The Online route answers an empty list with hidden,
count and audience; the feed answers presenceHidden and
presenceAudience.

Before this the app would have rendered the withheld list as "the
island is empty" on a full server. Now:

* RustOnlineDto and RustEventListDto carry the flags; the repository
  and view model keep the whole answer instead of its rows.
* the Online tab says "2 players online" and who can see the names;
  the feed says, once above the rows, that joins, deaths and chat are
  not shown.
* an older module without the flags decodes as visible, as before.

Walked on an emulator against a core with the module installed: the
withheld panel and the feed note as a signed-in player at the staff
default, and the names appearing on the next poll after the fleet was
widened to signed-in - which also proves the app's bearer session
reaches the module's viewer check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #49
feat(notifications): a Rust notification on a phone (module-rust phase 11)
All checks were successful
PR Checks / android-build (pull_request) Successful in 8m54s
640b423dfa
Three places the app still assumed its site was a UO shard (D69-D71):

- The linked-account check behind personal streams asked module-uo's
  /player/shard/accounts. On a Rust site that failed, and the whole raid
  alert row was disabled in every channel. It now asks the site's own
  module by capability, and the link only holds back switching push ON;
  switching anything off is never refused.
- A tickle whose ref names an inbox row is titled from that row, pulled
  over the authenticated inbox API (wake-and-pull). The lock screen shows
  only the per-stream title. Each tickle runs in its own job, because
  collectLatest would cancel a pull on the next tickle.
- /player/rust and /rust/servers/<id>?tab=<tab> open natively; a tab the
  app does not have (clans) still hands off to the browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #50
feat(rust): the map on a phone (module-rust phase 15, M17)
All checks were successful
PR Checks / android-build (pull_request) Successful in 9m53s
09eb101c52
A fifth tab on the Rust server screen, between Online and Wipes, shown
when the site's module declares `map` under the app's one capability
rule (D122). `?tab=map` opens it; on a site without a map it opens the
feed.

The phone draws the map itself (D121): Coil loads the picture once per
map as RGB_565, and a Compose Canvas draws the grid and the markers,
with pinch, pan and double tap. The frame is §30.3's transform, ported
from the web's mapGeometry.js with its test cases. Markers keep their
size on screen; a zone is drawn on the ground. Nothing here gates a
layer: map/live is projected per viewer on the server, and the app
draws what it is sent.

- The legend: a switch per layer the viewer was sent, and who can see
  each one they were not (with the D113 line). The mates row offers
  the Steam link to a signed-in, unlinked viewer.
- The tap card: what a marker is, its grid square, and what its layer
  carried. A site-event marker whose run core's public calendar lists
  (D125) has Open event, which opens the app's own event page on that
  run (D123). A rehearsal's or unlisted event's marker says only
  "Site event".
- The poll: map/live every pollMs (10 s) while RESUMED on the Map tab
  (D124), and not at all for a viewer sent nothing that moves. A new
  mapKey re-reads the map, so a new seed's picture follows in place.
- Per account: the map's state is dropped and re-asked on a change of
  signed-in account, and an answer asked for the previous account is
  discarded when it lands.

Found on the emulator walk and fixed here: the reader's zoom was lost
on the way back from the event page, and again whenever the status line
under the map changed length (a resize was treated as a rotation).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #51
feat(rust): a Rust server's next wipe (module-rust phase 16, M18)
All checks were successful
PR Checks / android-build (pull_request) Successful in 9m28s
89c317f694
RustServerDto gains nextWipe { at, source }, null both when the operator
set no schedule and against a module older than phase 16. The server
list row and the server screen's header read "Next wipe Thu 1 Oct,
19:00 · in 6 days" in the phone's own zone, and "(rescheduled)" for a
one-off date the operator set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #52
feat(rust): chat titles on the leaderboard (module-rust phase 17, M19)
All checks were successful
PR Checks / android-build (pull_request) Successful in 9m38s
96aae7239b
Each leaderboard row carries `titles: [{ text, color }]` from a phase-17
module: the chat titles the player holds now. Drawn as chips under the
name, in the operator's colour with black or white ink chosen by WCAG
contrast (the website's rule). Absent from an older module, so the field
defaults to empty and nothing is drawn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #53
feat(rust): named NPC markers, bigger bosses, and an event's progress lines (runicnpc stage 8)
All checks were successful
PR Checks / android-build (pull_request) Successful in 13m36s
e6aa9526c8
- The map (D302, D303), as the site draws it:
  - an NPC's card is its name, or "(boss)" or "(a boss's add)";
  - the card names its event, with the title from the run resolver's
    existing calendar read;
  - a boss is bigger and magenta;
  - a boss outside any event is a world-layer marker with its name.
- The event screen renders the live occurrence's progress lines
  (MODULE_API 1.12.0, D304): "Bandit: 2 of 3 left", "Walk Juggernaut:
  59%". It re-reads quietly every 15 s while the event is live.
- DTOs gain the new fields with defaults, so an older site decodes as
  before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #54
feat(rust): coin balances on the Rust account screen (PLAN_REDESIGNS §11.2)
All checks were successful
PR Checks / android-build (pull_request) Successful in 11m28s
531f3b2c4b
The app half of the module's Economics (protocol 14): a Coins section on the
Rust account screen, from GET /player/rust/economics — one card per server,
each linked account's balance, "last known" when the server did not answer,
and "Economics is not installed on this server" where it is absent (D199).

A third independent read beside the links and the entitlements; a link
re-reads it, and a 404 (a module older than protocol 14) leaves the section
out rather than showing an error the player can do nothing about. A balance
never reported shows a dash, not zero.

testDebugUnitTest, lintDebug and assembleDebug pass locally; four new tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #55
feat(rust): your backpack on the Rust account screen (PLAN_REDESIGNS §11.3)
All checks were successful
PR Checks / android-build (pull_request) Successful in 9m32s
6a6fa5d76e
A Backpack section under Coins: one card per server (GET /player/rust/servers),
an Open button per linked account that reads GET /player/rust/backpacks once
per press and never polls, and the slots as pages of four-wide rows named by
the game (D340), blueprints by what they make (D341), overflow marked with the
take-back sentence. A 409 says Backpacks is not installed; the section is left
out with Coins on that read's 404, since both ship in protocol 14.

backpackPages/stackLabel are pure (BackpackLayout.kt) and tested, with the DTO
decode and the view model's open/read-again/close. Local gradle lint test
assembleDebug passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #56
feat(account): several Steam accounts on the account screen
All checks were successful
PR Checks / android-build (pull_request) Successful in 13m57s
c905aee94f
Steam sign-in (docs/website/STEAM_SIGNIN.md) lets a person link more than one
Steam account. Unlink went by provider, so it would have removed all of them;
it now sends the identity's id (DELETE …/identities/:provider?id=). A Steam row
reads "Steam: <persona name>" with its SteamID64 beneath, since Steam sends no
email. The sign-in sheet already lists providers from /auth/providers, so Steam
appears there with no change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Reviewed-on: #57
PLAN.md M20 step 1. strings.xml keeps the core strings; strings_uo.xml and
strings_rust.xml take the strings only that game's screens use. Android merges
the three into one R.string, so no key changes and no Kotlin changes.

Each string was classified by the screens that reference it, traced in code,
not by its key: a string any deployment's screen can show stays core.
error_shard_offline and error_feature_unavailable are the shared error-view
texts, so they stay core. All seven legacy push streams, server.status
included, are declared only by module-uo, so their titles are UO.

No text changes: all 578 elements are moved byte for byte (checked by
comparing name → element before and after).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
PLAN.md M20 step 2. The first screen a Rust community's player sees said
"Connect to your shard", and the next said "Sign in with your shard account".
The core strings now use core's words, as the website and runicgateway.com do:

- connect_*: "your community", "a Runic Gateway site"
- login_subtitle: "Sign in with your account on this site."
- error_shard_offline: "The game server is offline right now." It is the
  shared 503 text that both games' screens show.
- error_feature_unavailable and notifications_*: "This site …"
- push_channel_* and push_service_text: "Notifications from your community"

Two comments in the file said "shard" too, and now say "site". Keys are
unchanged. The UO screens keep their words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
PLAN.md M20 step 3. StringVocabularyTest reads strings.xml, strings_uo.xml
and strings_rust.xml in every values* directory, so translations are checked
too. It fails, naming the key and the word, when the core file holds either
game's word, the UO file holds a Rust word, or the Rust file holds a UO word.
The word list is runicgateway.com's checkTerms.mjs list, word for word. The
app never shows Discord's "guild ID", so that allowlist entry isn't carried
over.

Three more tests keep the guard honest:
- a fourth strings file with no owner is refused, because Android would read
  it and the guard would skip it;
- the three default files must exist and hold strings, so a wrong path can't
  pass on nothing;
- the matcher is checked against known hits and misses.

Run against the core file as it was before the rewording, it names exactly
the 14 strings that commit changed.

build.gradle.kts declares src/main/res as an input of the Test tasks. A
reworded string changes no class, so without that input Gradle left the test
task UP-TO-DATE and the guard did not run again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Found by the M20 trace, and fixed in this PR by the org lead's decision.
Both rows call /admin/shard/*, module-uo's admin mount, but they carried no
capability. So a moderator on a Rust-only site saw "Moderation" in the drawer
and opened a UO form ("Serial (0x…)", kick or ban by UO account) where every
action failed.

Both rows now declare Capability.SHARD, as the player rows do, so they are UO
screens and their strings belong in strings_uo.xml, where step 1 put them. A
new MenuCapabilityGatingTest case covers it: a moderator on a Rust-only
backend sees neither row but keeps the Dashboard, and on a UO backend sees
both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
feat(rust): print the weapon name the module sends in the feed
All checks were successful
PR Checks / android-build (pull_request) Successful in 14m33s
c41aff9b2e
PLAN.md M20 step 4. Module-Rust#60 gives every event frame a weaponName: the
game's own name for a ranged weapon, or the prefab name as words. The feed
printed the prefab name as words itself, which read "with ak47u entity".

weaponName() in RustFormat.kt is the Kotlin half of module-rust's format.js
weapon(). It prints frame.weaponName. Against a module too old to send it,
it drops the .entity / .weapon / .deployed ending before prefabName(), as
the website does, so an older module's kill reads "with ak47u".

Tests: a kill with weaponName, the same kill from an older module, and the
fallback's edges (blank name, each ending, an ending only at the end, no
weapon).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Merge pull request 'feat(strings): the app's words for every game (M20)' (#58) from feat/app-vocabulary into edge
All checks were successful
PR Checks / android-build (pull_request) Successful in 3m9s
478206e0bd
Reviewed-on: #58
whitlocktech approved these changes 2026-10-23 02:08:36 -05:00
whitlocktech scheduled this pull request to auto merge when all checks succeed 2026-10-23 02:08:42 -05:00
whitlocktech merged commit 3d01871c72 into main 2026-10-23 02:09:14 -05:00
Sign in to join this conversation.
No description provided.