Skip to content

anchildress1/vestige

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

60 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Vestige

A brain tracker that won't blow smoke up your ass. Gemma 4, Android, fully local.

Built for the Gemma 4 Challenge β€” submission category: Build with Gemma 4.
Canonical product spec lives under docs/; see AGENTS.md for AI agent rules.

CI GHA Status CodeQL GitHub Actions Workflow Status
Sonar Violations Sonar Coverage Sonar Tests

Vestige social banner

Table of Contents


About

Vestige observes behavioral traces and surfaces patterns without therapy framing, mood scoring, or wellness vocabulary. It runs Gemma 4 E4B locally via LiteRT-LM β€” your voice never leaves the device, the audio bytes are discarded after inference, and entries can be exported as readable markdown at any time.

The positioning is deliberate: cognition tracker, not journal app. Patterns are sourced β€” every claim cites the entries it counted. Full product spec: docs/concept-locked.md.


Status

The full loop is implemented and runs on-device: voice / typed capture β†’ Gemma 4 E4B β†’ 3-lens extraction β†’ convergence resolver β†’ ObjectBox, with deterministic pattern detection. EmbeddingGemma embeds each entry by its tone word and powers Vocab Drift: the VOCAB_FREQUENCY cluster mints on the demo corpus (the Drained Vocab Frequency pattern, verified on-device). The earlier ranked content-retrieval path was cut when the embedding axis moved to the tone word (see Known Limitations). Entry Detail surfaces the model's actual work: the three-lens read, the picked archetype, the tone word, and a collapsible raw per-lens model-output view. Capture, history, pattern list + detail, settings, model-status, and onboarding model-download are all built against the canonical spec under ./docs/. Pattern lifecycle is Skip / Drop / Restart β€” closure is model-detected only (v1.5, see backlog.md Β§pattern-auto-close). The active phase is on-device prompt tuning against a seeded demo corpus; risk through phases 1–3 was managed via five stop-and-test points (STT-A–E), and on-device tuning since (STT-F–H) lifted the model off its audit default into differentiated archetypes. Screen-flow diagrams: docs/diagrams/user-flows.md.


Features

Feature What it does
Voice capture AudioCapture β†’ Gemma 4 E4B native audio modality. No third-party STT. Audio bytes discarded after inference.
Multi-lens extraction Each entry runs 3 lens passes (Literal / Inferential / Skeptical), each covering all 5 surfaces in one call; a convergence resolver votes every field consensus / candidate / ambiguous / consensus-with-conflict β€” tags, archetype, stated commitment, recurrence, and tone word. See ADR-002.
Model transparency Entry Detail exposes the model's actual work β€” the picked archetype, the per-lens read, the resolved field grid, and a collapsible raw per-lens model-output block. Nothing is hidden behind a score.
Tone & vocab drift The Inferential lens names a one-word tone per entry; entries are embedded by that tone word, and recurring/related tones surface as an EmbeddingGemma cluster (Vocab Drift). Minting on the demo corpus β€” the Drained Vocab Frequency cluster, verified on-device.
Three personas Witness / Hardass / Editor β€” tone-only variants. They do not fork extraction logic.
Pattern detection Six primitives counted over the last 90 days; sourced (counts, dates, snippets), no feelings or motivation interpretation. See ADR-003.
Storage ObjectBox is the internal source of truth. Export renders readable markdown from rows on demand.
Pattern lifecycle Skip (returns in 7 days) / Drop (noise, archived) / Restart, with Undo. Closure is model-detected only β€” v1.5.
Export System-picker (SAF) zip of per-entry markdown. No storage permission; failures surface, never silent.
Local-only Zero outbound network calls during normal operation; model download is the only network event. Verified with tcpdump.

Tech Stack

  • Kotlin 2.3.21 + Jetpack Compose (BOM 2026.05.00), AGP 9.2.1
  • Gradle KTS + version catalog (gradle/libs.versions.toml)
  • Gemma 4 E4B via LiteRT-LM (com.google.ai.edge.litertlm:litertlm-android:0.11.0), on-device only
  • ObjectBox 5.4.2 (entries, tags, patterns, vectors) + generated markdown export
  • Android minSdk 31 / targetSdk 35 / compileSdk 36, JVM toolchain 25 (Java source/target compat 17)

Architecture

Four-module split with manual constructor injection through a single AppContainer (ADR-001 Β§Q1–Q2). Foreground call returns transcription + persona-flavored follow-up fast; the 3-lens convergence pass runs in the background and writes consensus / candidate / ambiguous fields when it lands.

flowchart TB
    User([User]) -- voice --> Audio
    User -- "type Β· persists directly, skips FG (ADR-018)" --> BG

    subgraph onDevice["on-device only β€” no network at runtime"]
      Audio["AudioCapture<br/>mono 16 kHz float32 Β· 30 s cap"]
      FG["ForegroundInference<br/>fast transcription + inline persona follow-up"]
      BG["BackgroundExtractionWorker<br/>3 sequential lens passes Γ— 5 surfaces"]
      Gemma[("Gemma 4 E4B<br/>via LiteRT-LM")]
      Resolver["Convergence Resolver<br/>consensus Β· candidate Β· ambiguous Β· w/ conflict"]
      ObjectBox[("ObjectBox (source of truth)<br/>entries Β· tags Β· patterns Β· vectors")]
      Export[("Export renderer<br/>markdown + JSON snapshot")]
      Patterns["Pattern Detection<br/>6 primitives Β· 90-day window Β· every 3 entries"]

      Audio --> FG
      FG -- "prompt + audio" --> Gemma
      Gemma -- "transcription + follow-up" --> FG
      FG --> ObjectBox
      FG -. "hands off" .-> BG
      BG -- "3 lens prompts" --> Gemma
      Gemma -- "lens output" --> BG
      BG --> Resolver
      Resolver --> ObjectBox
      ObjectBox --> Patterns
      Patterns --> ObjectBox
      ObjectBox --> Export
    end
Loading

Module boundaries: :app (UI), :core-inference (LiteRT-LM + lens composition), :core-storage (ObjectBox + markdown), :core-model (domain types). Ownership detail: docs/architecture-brief.md.


Project Structure

.
β”œβ”€β”€ app/                       # :app β€” Compose UI, navigation, AppContainer (manual DI)
β”œβ”€β”€ core-model/                # :core-model β€” domain types, manifests, no Android deps
β”œβ”€β”€ core-inference/            # :core-inference β€” LiteRT-LM engine + 3-lens composition
β”œβ”€β”€ core-storage/              # :core-storage β€” ObjectBox rows + export markdown renderer
β”œβ”€β”€ docs/                      # canonical product/architecture/UX spec
β”‚   β”œβ”€β”€ README.md              # reading order + file inventory
β”‚   β”œβ”€β”€ PRD.md                 # P0/P1/P2 requirements + phase schedule
β”‚   β”œβ”€β”€ concept-locked.md      # full product spec
β”‚   β”œβ”€β”€ adrs/                  # ADR-001..018, no 009 (stack, lenses, patterns, lifecycle, runtime, design, …)
β”‚   β”œβ”€β”€ architecture-brief.md
β”‚   β”œβ”€β”€ design-guidelines.md
β”‚   β”œβ”€β”€ ux-copy.md             # locked microcopy authority
β”‚   β”œβ”€β”€ spec-pattern-action-buttons.md
β”‚   β”œβ”€β”€ sample-data-scenarios.md
β”‚   β”œβ”€β”€ backlog.md             # deferred features w/ unblock conditions
β”‚   └── stories/               # phase-1..7 build queue
β”œβ”€β”€ poc/                       # Compose-port reference (screenshots)
β”œβ”€β”€ gradle/                    # version catalog + dependency verification
β”œβ”€β”€ scripts/                   # doctor, lint, secret scan helpers
β”œβ”€β”€ AGENTS.md                  # AI implementor guardrails (authoritative)
β”œβ”€β”€ CLAUDE.md                  # Claude Code β†’ AGENTS.md pointer
β”œβ”€β”€ lefthook.yml               # pre-commit / commit-msg / pre-push hooks
β”œβ”€β”€ Makefile                   # local CI surface
β”œβ”€β”€ LICENSE
└── README.md

Four-module split per ADR-001: :app (UI) depends on :core-inference, :core-storage, and :core-model; the core modules do not depend on :app.


Getting Started

Prerequisites

Tool Required for Install
JDK 25 LTS (Temurin) Gradle runtime + Kotlin toolchain brew install --cask temurin
Android SDK + adb build + install on device Android Studio, or brew install --cask android-commandlinetools
System Gradle (optional) regenerating the wrapper jar (make bootstrap-wrapper); not needed for routine builds, since gradle/wrapper/gradle-wrapper.jar is committed brew install gradle
lefthook git hooks brew install lefthook
gitleaks secret scan brew install gitleaks
actionlint workflow lint brew install actionlint
ktlint format + lint Kotlin brew install ktlint
detekt static analysis Kotlin brew install detekt
gh repo ops brew install gh

ANDROID_HOME must be set and $ANDROID_HOME/platform-tools must be on PATH so adb resolves. Gradle dependency verification is pinned in gradle/verification-metadata.xml; refresh it only when changing dependencies. SonarCloud analysis runs through the Gradle sonar task in CI rather than a standalone scanner config, because Android builds deserve one source of truth at a time.

Build

make setup      # bootstrap gradle wrapper, install lefthook hooks
make doctor     # verify local toolchain and environment variables
make build      # assemble debug APK
make test       # unit tests for changed modules (75% coverage gate runs in `make verify`)
make lint       # ktlint + detekt + Android lint
make verify     # lint + test + build + staged secret scan
make ci         # full local check (lint + test + build)
make clean

make setup is the hook/bootstrap target. make install is now device-only and requires adb; it does not install lefthook anymore.

Run on a device

Reference device: Galaxy S24 Ultra. External devices are best-effort; submission promise is Android 14+, 8 GB RAM, 6 GB free storage.

make install    # assemble + adb install debug APK without wiping app data
make reinstall  # reinstall APK, push models, seed debug fixtures, tail logcat

Demo data on hardware (dev builds only)

Just want to try Vestige? Install the release APK from the GitHub release and onboard normally β€” you get a clean first-run (Nothing on file.) and capture your own entries. The seeded demo corpus below is a dev-only reproduction aid and never ships in the release APK.

The demo seed is a deterministic ~36-entry corpus loaded by DebugPatternSeeder through an ADB-triggered DebugSeedReceiver that exists only in debug builds (app/src/debug/…, registered in the debug manifest overlay). It writes rows straight to ObjectBox and marks onboarding complete, so the seeded build opens past first-run with history already populated.

make reinstall ENV=dev            # clean debug install + push models + seed + launch + tail logcat
make reinstall ENV=dev EXTRACT=1  # same, plus run background extraction so cards carry real lens receipts
make seed-entries                 # re-seed an already-installed debug build (no reinstall)
make seed-entries EXTRACT=1       # re-seed + run extraction
  • What EXTRACT=1 actually does β€” and why it's slow. Seeding writes each entry as text straight to ObjectBox and bypasses the foreground model call entirely β€” there is no audio, no transcription, and no model foreground response. EXTRACT=1 then runs the live background extraction once per seeded entry (the same sequential 3-lens convergence a real capture runs). Expect a full GPU load of ~30 s per entry while it churns. Across the ~36-entry corpus that's β‰ˆ18 minutes at 30 s/entry β€” in practice budget ~25–30 minutes: the measured full 3-lens pass is ~44 s/entry (STT-F), and entries that mint a pattern add an observation + title pass on top. Watch DebugSeedReceiver / Vestige in logcat for seed complete. If you don't want to sit through that, install the clean release APK instead (no seed, no extraction).
  • Without EXTRACT=1 entries seed instantly but stay PENDING β€” no lens receipts, no patterns, no vocab clusters. Fine for a History/layout check, useless for the extraction and pattern beats.
  • Idempotent β€” each seed wipes the entry / tag / pattern / cooldown tables and reloads, so re-running never duplicates.
  • Seed timestamps are local wall-clock spanning 2026-04-25 β†’ 2026-05-22 (entry prose names clock times like "2am", so the loader seeds in the device zone, not UTC).

What you'll see on-screen after seeding:

  • History β€” the full timeline populated, newest first.
  • Patterns β€” a Tuesday-afternoon meeting-crash recurrence (template Crashed) forms once the third supporting entry lands, with a sourced callout. A Thursday-evening cluster is a deliberate negative control β€” same time slot, unrelated end-of-day logistics. Temporal detection is deterministic, so it does mint a TEMPORAL_RELATIVE "Thursday evening" pattern from the shared weekday + time block alone (β‰₯ 3 distinct dates); the point of the control is that it surfaces as a benign time-block observation, not a cognitive recurrence β€” the demo shows the model can tell "I always log at 5pm" from "I crash every Tuesday."
  • Vocab Drift (requires EXTRACT=1) β€” intended to group entries that share no keywords ("drained", "wiped out", "running on empty", "depleted", "burnt out", "brain fog") into one exhaustion cluster, with a separate positives cluster ("locked-in", "clear", "good", "sharp") β€” the embedding proof. On the demo seed this mints a VOCAB_FREQUENCY pattern (Drained Vocab Frequency), verified on-device 2026-05-24 β€” the embedding proof is live.
  • Type "I hate demos" as a live entry and it joins the seeded demo-dread cluster.

Required local artifact filenames match core-model/src/main/resources/model/manifest.properties:

~/Downloads/gemma-4-E4B-it.litertlm
~/Downloads/embeddinggemma-300M_seq512_mixed-precision.tflite
~/Downloads/sentencepiece.model

For a production first-run check with no pushed model and no fixtures:

make reinstall ENV=prod

One-time phone setup

  1. Settings β†’ About phone β†’ tap Build number 7 times to unlock developer options.
  2. Settings β†’ Developer options β†’ enable USB debugging. Optional: enable Wireless debugging if you'd rather not cable up.
  3. (Optional) Stay awake while charging β€” speeds iteration.

Connect

USB:

adb devices
# expect: <serial>    device
# if "unauthorized", accept the prompt on the phone (check "Always allow")

Wireless (Android 11+):

# On phone: Developer options β†’ Wireless debugging β†’ Pair device with pairing code
adb pair <ip:port>      # use the pair port + 6-digit code shown on phone
adb connect <ip:port>   # then use the connect port shown on phone
adb devices             # verify

Install + launch

./gradlew :app:installDebug
adb shell monkey -p dev.anchildress1.vestige -c android.intent.category.LAUNCHER 1

installDebug builds and installs in one step. The monkey invocation just opens the launcher activity without you having to tap the icon.

Manual APK install:

./gradlew :app:assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk

Tail logs

adb logcat -s "VestigeApplication:*" "AndroidRuntime:E" "*:F"

Reinstall clean

adb uninstall dev.anchildress1.vestige
./gradlew :app:installDebug

Troubleshooting

Symptom Fix
adb: command not found export PATH="$ANDROID_HOME/platform-tools:$PATH" in your shell profile
INSTALL_FAILED_NO_MATCHING_ABIS APK didn't include arm64-v8a. Verify with unzip -l app/build/outputs/apk/debug/app-debug.apk | grep arm64-v8a
INSTALL_FAILED_USER_RESTRICTED Disable Verify apps over USB in Developer options
App crashes on launch adb logcat AndroidRuntime:E *:S for stack trace
Themed monochrome icon on Android 13+ Expected β€” placeholder icon; final design lands Phase 6

Configuration

v1 has effectively zero configuration. The model artifact downloads on first launch over Wi-Fi (3.66 GB) into Context.filesDir/models/. A cheap presence + size probe resolves the artifact state without hashing the multi-GB file on the UI thread; a full-size artifact is then SHA-256-verified off-thread before readiness flips to Ready, so a checksum-corrupt full-size file falls back to Loading rather than a false Ready (AppContainer.probeModelReadiness). The engine itself loads lazily on the first inference, not proactively, because proactive pre-warm regressed into a startup GPU-init crash (ADR-012). Persona default is set during onboarding and changeable from settings. Pattern analysis runs periodically β€” every 3 completed entries (ADR-014) β€” with a per-pattern callout cooldown of 3 (ADR-016), hardcoded for v1. No env vars, no .env file, no remote-config layer β€” adding any of those is a P0 violation per ADR-001 Β§Q7.


Security & Privacy

Privacy is the differentiator, not a side feature.

  • Zero outbound network calls during normal operation. The model download (one-time, Wi-Fi, Hugging Face) is the sole network event. Verified with tcpdump; the proof clip is part of the demo video.
  • Audio bytes discarded after inference. Transcription persists as text (the entry_text substrate); raw audio never lands on disk as product data.
  • No analytics, telemetry, crash beacons, remote config, CDN fonts. Crash logs are local; user can export from settings.
  • Network enforcement is code, not vibes. A NetworkGate abstraction owns the only HTTP path; default state is SEALED, OPEN only during model download. Direct OkHttpClient / URL.openConnection construction outside NetworkGate is forbidden and grep-checked in CI. See ADR-001 Β§Q7.
  • No proactive crisis triage. If the user explicitly asks for self-harm help, a static local message points to local emergency services. No diagnosis, no network call.

Contributors: do not introduce dependencies that pull in Firebase, Crashlytics, Segment, Mixpanel, or any analytics SaaS. Do not add a fonts CDN. Do not call out to a cloud LLM as a fallback. Any of these invalidates the entire submission.


How to Contribute

PRs are not accepted through the submission deadline (2026-05-24). Issues are welcome β€” use the GitHub issue tracker. Post-submission, see AGENTS.md and backlog.md for the contribution surface.

Branches and commits follow AGENTS.md and the repo conventions: atomic, GPG-signed, a Generated-by: footer on AI-authored commits (e.g. Generated-by: claude-opus-4-7), Conventional Commits, never on main.


What's Next

v1 ships 2026-05-24. Deferred features live in backlog.md β€” v1.5 / v2 / STT-conditional, with explicit unblock-conditions per entry. No "coming soon" handwaving.


Known Limitations

What v1 actually does, stated straight.

  • Embeddings power Vocab Drift only β€” no semantic search. EmbeddingGemma 300M embeds each entry by its tone word (the felt quality), and the VOCAB_FREQUENCY cluster mints on the demo corpus β€” Drained Vocab Frequency is a live active pattern, verified on-device 2026-05-24. The earlier ranked content-retrieval path (RetrievalRepo β€” keyword + tag + recency + cosine) was cut when the embedding axis moved to the tone word: a content query can't score against a feeling vector, and it was never wired to a live surface. Semantic search across entries is not a v1 feature; embeddings exist to surface tone clustering, nothing more.
  • Voice captures cap at 30 s. AudioCapture emits one final chunk at 30 s; the >30 s multi-chunk path is deferred (backlog.md β†’ multi-chunk-foreground). An audio cue at ~28 s warns before the cap fires.
  • First inference is cold (~15 s). The engine loads lazily on the first capture β€” proactive pre-warm was reverted after it regressed into a startup GPU-init crash (ADR-012). Subsequent calls run ~7–11 s on E4B GPU; a full background 3-lens extraction is ~44 s/entry.

License

Polyform Shield 1.0.0 + Supplemental Terms. Source-available, not open-source: read it, run it, modify it for personal or internal use. Don't sell it, don't ship a paid product on top of it, don't use it to compete with Vestige itself. The full grant and exceptions are in LICENSE β€” that is the legally-binding version; this paragraph is just the plain-English flavor.


Acknowledgements

  • Google's Gemma team for the E4B model and the native audio modality that made this entire concept tractable on a phone.
  • The LiteRT-LM team (google-ai-edge/LiteRT-LM) for the Android SDK that lets Kotlin code run a multimodal LLM without writing JNI by hand.
  • ObjectBox for an embedded DB that does not require an SQL ceremony.
  • Hugging Face / litert-community for hosting the pre-converted gemma-4-E4B-it-litert-lm artifact.
  • The Polyform Project for licenses that admit not every project is MIT-shaped.
  • DEV (dev.to) for hosting the Gemma 4 Challenge β€” the venue this whole build was aimed at, and a community that rewards shipping over hand-waving.
  • Major League Hacking (MLH) for backing the challenge and the broader hackathon community that makes deadlines like this one fun instead of just terrifying.

Author

Ashley Childress (@anchildress1). Vestige is an Android side-build aimed at the Gemma 4 Challenge "Build with Gemma 4" prize. The brand voice and product opinions are entirely intentional.

Contributors

Languages