Three build-hygiene fixes that turned up while explaining the pathspec.
**version.sh derives from what SHIPPED.** It read bare HEAD, so any commit
moved the version — including one touching only CI or a README. Rules 148
and 149 both specify the pathspec form. Now a denylist, and the direction
is the point: as an allowlist the list must be updated by whoever adds a
directory and nothing fails if they don't, so the failure mode is a changed
artifact keeping its old version silently on a green run. Inverted, new
content counts by default.
android/ is deliberately NOT excluded, and that is the subtle part. This
repo ships TWO artifacts from ONE derivation: android/ is in no server
image, but it is the APK's entire source, and excluding it would stop an
Android-only commit from moving the APK's own version — the silent
downgrade the versioning rework exists to prevent. So the list is the
union: exclude only what ships in neither, and accept that an Android
commit also nudges the server's reported version. Over-inclusion across the
two, which is the harmless direction. roundtable/roundtable-android each
keep tighter lists because they are one-artifact repos; don't copy theirs.
**.dockerignore excluded the wrong CI directory.** It named .forgejo/ and
.github/, neither of which this repo has. Gitea Actions reads .gitea/, so
the one directory that exists was the one not excluded. The "Flutter mobile
client" block had also lost its PATTERN when flutter_client/ was deleted,
leaving a comment describing an exclusion that was not happening — android/
never took its place, so 4.1MB of Gradle project entered the context and
busted the `COPY . .` layer on every Android-only change. bin/ excluded too.
**bin/minstrel was tracked** — an 18MB binary last refreshed by a commit
about web test mocks, and re-dirtied by every `make build` since. Untracked
and ignored; the file stays on disk.
Guards are behavioural rather than textual: they build throwaway repos with
pinned commit timestamps and run version.sh against them, so they break when
the derivation changes rather than when the wording does. Falsified — drop
the .gitea exclusion and the CI-only commit moves the version; add an
android exclusion and an Android commit stops moving it; exclude everything
and a source commit refuses.
One honest note on the refusal test: the script already refused an empty
result via the downstream date check, so the new explicit check improves the
diagnostic ("no commit touches the shipped file set — shallow clone?") and
not the safety. The test pins the property, which is defended in depth.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH
Superseded by the M8 native Android rewrite. Last touched 2026-05-31,
no workflow has built it since flutter.yml was removed, and rule #22
says a replaced path goes rather than lingering as something a reader
has to work out the status of. 245 files, ~24.6k lines.
Config references go with it: the .gitignore block (and its now-empty
"# Flutter" header), the .dockerignore entry, and renovate's ignorePaths
entry, which was suppressing dependency scanning for a directory that
no longer exists.
ci-requirements.md said ci-flutter "will retire once that directory
goes". It has gone, so the doc now says so -- CI-Runner can drop the
image, and nothing in this repo needs a Flutter toolchain.
One thing is kept rather than deleted: shared/fabledsword.tokens.json.
It lived under flutter_client/shared/ but was never Flutter's property
-- it is the canonical statement of the palette, the only place the
dark, light and flat cohorts are written down together, and
FabledSwordTokens.kt names it as its source of truth. Losing it would
have been collateral damage, so it moves to the repo root with a README
saying what it is and that neither client generates from it. That
comment in FabledSwordTokens.kt is repointed here.
What deliberately does NOT change: `runs-on: flutter-ci` in android.yml
and release.yml. That is a runner LABEL, not a path -- the Android jobs
schedule on it while pulling ci-android:36, per the label/image split
ci-requirements.md documents. Removing it would break scheduling for a
cosmetic win, so the doc now spells that out beside the retirement note.
Left for #2710: 64 files whose comments still name flutter_client/
paths. Sweeping them here would have buried the deletion, and each
needs a judgement -- keep the substance and drop the dead path, delete
pure "ported from" bookkeeping, or leave design rationale that happens
to mention the Flutter build.
Project-level AI-tool instruction files (CLAUDE.md, AGENTS.md,
GEMINI.md, .cursorrules, .windsurfrules, .aider.conf.yml) are
operator-scoped working notes, not project artifacts. Other
contributors don't need them, and committing them conflates
operator preferences with shared project conventions.
CLAUDE.md remains on the local disk (untracked) so Claude Code
keeps loading it for this checkout; new clones simply won't have
it, which is the desired state.
- rename .forgejo/workflows/ to .gitea/workflows/ (git mv preserves
history); Gitea Actions reads either path, but the directory now
matches the active platform
- collapse ANDROID_STORE_PASSWORD + ANDROID_KEY_PASSWORD into a
single ANDROID_KEYSTORE_PASSWORD secret (PKCS12 keystores require
the two passwords to be identical, so the duplication carried no
information); build.gradle.kts still reads two env vars to stay
format-agnostic, both now sourced from the same secret in CI
- ignore android/*.keystore, android/*.keystore.*, android/*.jks,
android/keystore.properties so the regenerated signing material
never reaches a remote on accident
- update prose references from Forgejo to Gitea in CLAUDE.md and
the docs; the historical "migrated from Forgejo" note in CLAUDE.md
is kept intentionally; forgejo/upload-artifact@v3 action refs are
left untouched (canonical artifact action, resolves cleanly under
Gitea Actions)
Operator side: ANDROID_KEY_ALIAS, ANDROID_KEYSTORE_PASSWORD, and
ANDROID_KEYSTORE_B64 secrets registered in Gitea before this lands.
Previous commit (0b72827) accidentally swept the local
android/.gradle, android/build, android/app/build, and
android/local.properties into the index via `git add android/`.
Root .gitignore only had entries for flutter_client/android/
paths, not the new native android/ tree.
Removes the cached files via `git rm --cached` and extends
.gitignore to cover the native Android Studio output dirs
(.gradle, .kotlin, .idea, build, app/build, local.properties,
*.iml). Source code is unaffected — only build-output and IDE
artifacts get untracked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final phase of the in-app update flow. release.yml on tag pushes
fetches the APK that flutter.yml is attaching to the same release,
drops it into client/ in the build context, and the Dockerfile's
COPY client/ /app/client/ bakes it into the image.
### How it sequences
flutter.yml and release.yml both trigger on tag pushes and run in
parallel on different runners (flutter-ci vs go-ci). flutter.yml
typically finishes APK build + release attachment in 2-5 min.
release.yml polls the release page for up to 15 min for the APK to
appear, then proceeds. If the APK never lands (flutter.yml failure,
network hiccup), release.yml emits a warning and ships the image
without the bundled APK — server returns 404 from /api/client/version,
banner stays hidden, manual download from the release page still
works. Graceful degradation, not a blocker.
### Repo shape
- client/.gitkeep + client/README.md so the directory exists in git
and the COPY in the Dockerfile always finds something to copy
- .gitignore excludes client/minstrel.apk + .version so accidental
commits don't bloat the repo
- Dockerfile: COPY --chown=minstrel:minstrel client/ /app/client/
### Why polling vs cross-workflow trigger
Forgejo Actions' workflow_run support varies by runner version; the
polling approach is universally compatible. If/when we standardize on
a runner that handles workflow_run cleanly, the polling step can
become a `needs:` dependency.
Closes#397.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Workspace flatten brought these operator-scoped state dirs (Claude
Code per-machine settings and remember-skill memory artifacts) into
the repo's working tree. Neither belongs in git — the canonical
project memory lives under ~/.claude/projects/ outside the repo.
Agent-authored design specs and implementation plans are kept local
on the working machine; the canonical record lives in commit messages
and the running code. The 48 historical files remain in git history
and can be retrieved via git checkout <sha> -- docs/superpowers/ if
ever needed; they just stop accruing on the dev tip.
iOS + Android targets only; desktop disabled (per spec, Tauri-wrapped
SvelteKit handles desktop in v1.1). Riverpod + dio + just_audio +
audio_service + go_router pinned in pubspec. Smoke test confirms the
ProviderScope wiring boots a Material app.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First pass of the web UI design doc from the M1 close-out brainstorm.
Covers the stack (SvelteKit static + Go embed), auth model
(cookie + bearer from one endpoint), /api/* surface, shell layout
(sidebar + bottom player), first-cut feature scope, and how later
web UI work threads into M2–M5 alongside each backend milestone
instead of landing as a post-hoc M6.
Also ignores the local .superpowers/ brainstorming companion cache.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>