Instructions for AI coding agents working in this repo. Cross-tool standard (agents.md) — read by Cursor, Codex, Copilot, Jules, Devin, Factory, Aider, Zed, recent Claude Code, and others. If you use a tool that only reads CLAUDE.md, create a one-line local CLAUDE.md containing See @AGENTS.md (Claude Code resolves @ imports). Not committed — CLAUDE.md is gitignored so each contributor sets it up if needed.
Bull Bitcoin Mobile: self-custodial Bitcoin + Liquid + Lightning wallet. Flutter/Dart, BLoC, Drift/SQLite, GoRouter, get_it DI. See README.md for product details.
Repo doc map: README.md = product · ARCHITECTURE.md = layer model & design rules (read its "Entry points & code tour") · AGENTS.md (this file) = toolchain, conventions, commit/test rules · FEATURES.md = cross-feature dependency graph.
Composition root (where to wire a new feature): lib/main.dart (Bull.init() → initLocator() → runApp) → lib/locator.dart (get_it; AppLocator.setup registers core, then every <Feature>Locator.setup) → lib/router.dart (GoRouter; AppRouter.router spreads each <Feature>Router's routes). A new feature is added in those last two files.
- Always
fvm flutter/fvm dart. Never bareflutter/dart. The pinned SDK lives in.fvmrc; using a global SDK silently breaks builds. - Use the makefile. Don't reinvent commands:
make deps—fvm flutter pub get --enforce-lockfilemake analyze—fvm flutter analyze --fatal-warnings --fatal-infos(matches CI; same check the pre-commit hook runs)make checks— the full CIchecksjob locally: analyze +make bull-ui-check+make fix-check+make format-check+ unit tests; CI invokes these same targets, so green here means that job is green in CI. Each sub-target also runs individuallymake bootstrap— melos workspace bootstrap (wrapsfvm dart run melos bootstrap)make build-runner— codegen (freezed, json_serializable, drift, flutter_gen)make translations—fvm flutter gen-l10nmake setup— full first-time setup (clean + deps + build-runner + translations + hooks + ios pods on macOS)make unit-test/make integration-test/make testmake drift-migrations— after changing Drift schemamake android/make android release— reproducible buildmake android beta— beta tester channel APK (.betaapplicationId, installs alongside production)make verify— verify a built APK matches a published releasemake build-runner-watch— codegen in watch mode during dev
- Analyze the whole project, never specific files.
fvm flutter analyze(no path arg). CI runs--fatal-warnings --fatal-infos(analyze_and_test.yml) — match it locally. fvm dart fix --dry-runmust printNothing to fix!before commit. If not, runfvm dart fix --applyand stage the result.- Pre-commit hook is the floor. It runs analyze + dart fix dry-run (
.git_hooks/pre-commit). Never--no-verify. If it fails, fix the cause.
The repo is migrating incrementally to a melos pub-workspace. The Flutter app remains at the repo root through useRootAsPackage: true; current members are the bull_ui design system, its dev-only catalogue, the pure-Dart primitives package, and the pure-Dart bull_payjoin package. The makefile remains the canonical entry point across the workspace.
- Run melos through the makefile (
make bootstrap), which wrapsfvm dart run melosso the pinned SDK (.fvmrc) is used. Never type baremelos(wrong SDK). For melos subcommands without a make target yet, usefvm dart run melos <cmd>— and add a make wrapper if it becomes routine. - The makefile stays canonical for daily commands. melos does not replace it:
make depsis stillfvm flutter pub get --enforce-lockfile, and the reproducible build chain (Containerfile.app, build-android.yml) does not run melos. melos is adev_dependencyonly — never compiled into the app, so the reproducible APK is unaffected. packages/andfeatures/are reserved homes for the migration (exception to rule #14 below): pure-Dart shared-foundation packages inpackages/(shared domain likewallet/secrets+ infrastructure likestorage/electrum, no Flutter UI —packages/holds the shared foundation, withlib/core's infra-only spirit per rule #7 preserved for the infra packages); Flutter feature packages (send,receive,buy,sell, …) mounted by the root shell infeatures/. Apackages/member may depend on Flutter only to own a sealed UI component (e.g.secrets'MnemonicView, which renders the mnemonic without exposing it). As code is extracted, each new package getsresolution: workspaceand is added to aworkspace:key in the root pubspec; the root keepsuseRootAsPackage: true(this combination is supported — see melos PR #927). See ARCHITECTURE.md "Monorepo Migration" for the full layout and the sealed-UI / encapsulation rules.- Workspace verification is measured, not assumed. Whole-project
fvm flutter analyzecovers the root and all members, andmake bootstrapsucceeds withenforceLockfile: true(verified 2026-07-30). Pub workspaces use the single rootpubspec.lock; do not add member lockfiles.make unit-testdispatches Flutter members tofvm flutter testand pure-Dart members tofvm dart test.
Read ARCHITECTURE.md ("How the codebase actually looks today" + "The architecture we tend toward") and FEATURES.md before adding or moving code. The repo is in active migration. Census (2026-06, 48 features): the dominant shape is ui → bloc → usecase → repository → datasource with most shared data + domain in lib/core/<domain> (≈30 domains, ≈26 repositories) and features mostly presentation + ui (bloc/cubit in ≈44); ≈21 features carry use-cases; ≈6 went fuller hexagonal (application/ + adapters/ + frameworks/ + interface_adapters/); public/ and watchers/ are barely used (≈1 each). The target below is what new code must follow. Existing code may violate these rules — do not replicate violations, but don't refactor unrelated legacy in the same PR either.
Some rules below are canonical (textbook Clean/Hex Arch), others are deliberate project conventions. Both are binding here, but knowing which is which helps when an external source contradicts a rule — the project rules win inside this repo, but cite the canonical source if the conversation moves to why. Project rules are explicitly labeled.
Hard rules:
-
Feature isolation — facade is our chosen pattern. Modular Flutter has several valid options (facade, abstract use-case interfaces, event bus, mediator); this codebase picks facade. Target location (per ARCHITECTURE.md):
<feature>/public/<feature>_facade.dart. Current legacy (e.g.labels_facade.dart): facade at feature root. New facades use the target location; don't move legacy ones in unrelated PRs. Never import another feature's internals (domain/,data/,presentation/) — thepublic/facade is the only importable surface. The facade is the interface — a concrete class, not anabstract interface class; its method surface +exportblock are the contract (add an abstract interface only for DIP-to-break-a-cycle or test mocking). It returns only the feature's published types (the entity itself when clean and stable, else a dedicated public DTO + mapper aslabelsdoes) — never adata/models/model, anapplication/-internal type, or another feature's entity, and the return type must be in theexportblock. Consumers wrap a facade call in their own use-case (rule #4). See ARCHITECTURE.md "The facade: a feature's public contract". -
Layer direction — one chain, always.
ui → presentation (bloc/cubit) → usecase (domain) → repository (domain interface → data impl) → datasource (data). A bloc never imports a repository or a datasource — it always goes through a use-case, the testable seam where data-layer errors are mapped to feature errors. Use-cases are always present but stay thin: orchestration only. Put each kind of logic in its home — invariants in domain entities/value objects; data-shaping (mapping, aggregation, caching) in the repository; orchestration in the use-case. Pushing data-shaping or invariants into a use-case is the "fat use-case" smell — move it down. The inward-only dependency rule is hexagonal-canonical; the MVVM-spine + repository is the official Flutter guide. See ARCHITECTURE.md "The architecture we tend toward". -
Watchers go through use cases, not repositories.
watcher → use case → repository. -
BLoCs are thin — in our architecture. No orchestration, no business decisions, no complex transforms; those live in use cases. (Note: bloclibrary.dev treats the bloc layer itself as the business-logic layer above repositories — our "BLoC is thin, use case below it" stance is a deliberate Clean-Arch refinement, not bloclibrary canon.) Project convention: never call another feature's facade directly from a BLoC — wrap it in your own feature's use case (in
domain/usecases/), and the BLoC calls that wrapper. This is the cure for "business logic in presentation" that ARCHITECTURE.md flags as the most common pitfall. -
Local widget state stays in
StatefulWidget. BLoC/Cubit is for business state only — what other layers care about. Form-field focus, animation controllers, expanded/collapsed panels, scroll positions: keep them in widget state. Mixing the two makes BLoC tests flaky and bloats state classes. -
Repository: abstract interface in
domain/, impl indata/— don't over-abstract the rest. Declare the contract asabstract interface class <Noun>Repositoryindomain/repositories/<noun>_repository.dart(the singlerepositories/folder); put the implementation indata/<noun>_repository_impl.dartnext to its datasources — no secondrepositories/folder. A datasource wraps one external system and is a private member of its repository (bloc/UI can't reach it). One repository is enough for trivial CRUD; add a separate datasource only for real transformation, multiple backends, or clear logic to separate — two layers that forward-call each other are noise. Boundary rule (never bends): a repository's public signatures — returns and params — use only domain types (entities, value objects,Result<Entity, …>). Wire models, library types (BDKLocalUtxo, LWK, a Drift row), JSONMaps and DTOs never cross the repository boundary — they stay insidedata/. (Legacy leak, don't replicate:WalletAddressRepositorytakes aWalletModelparam — adata/model in adomain/contract.) Entity and model are always two separate types (entity indomain/entities/, wire/persistence model indata/models/, mapper between) — this is the boundary above, not over-abstraction, so never collapse it into one class even when they look identical. The entity is self-validating (invariants in the constructor/factory; an invalid instance can't exist) and carries no serialization; the model is pure data (serialization only, no rules). Cap is two types per record — the five-types-per-record pattern (entity / value-object / primitive / request-DTO / response-DTO + a mapper between each) is verbosity the Flutter team flags as Conditional ("separate models adds verbosity… Use in large apps", recommendations); collapse that. Value objects are for correctness-critical primitives (amount/sats, address, fee rate), not every field. -
/lib/coreis infrastructure only. No feature/business logic. Generic primitives, drivers, helpers. -
Shared value objects go in
lib/core/primitives/(planned per FEATURES.md — folder doesn't exist yet at audit time, andSecret/Address/Amount/Fingerprintare not yet extracted as primitives). When you create a value object that more than one feature would reasonably use, put it inlib/core/primitives/from the start — that's how the primitives layer gets built. Before creating one, greplib/for an existing class with the same intent. -
Prefer rich domain models — methods that enforce invariants, not anemic DTOs mirroring DB rows. (Note: Fowler's anemic-domain anti-pattern is widely cited but not universal. This is our preference, aligned with ARCHITECTURE.md's "Anemic Domain Models" pitfall.)
-
No raw colors. Always pull from the theme. If the user describes a color in plain language, pick the closest theme color that works in both light and dark mode. Don't edit theme files unprompted.
-
Failures — one sealed family per feature,
*Failurenot*Error. Three words, kept apart:Exception= thrown infra (data layer, caught at the boundary);Error= adart:coreprogrammer bug, never caught (→ Sentry);Failure= a modeled, recoverable value indomain/. Define a sealed<Feature>Failure extends Failureindomain/<feature>_failure.dart, Flutter-free; cross-cutting modes (network, storage-locked, not-found, timeout, auth, device, insufficient-funds) come from the sharedsealed CoreFailureinlib/core/failures/(alongside theFailurebase; the legacylib/core/errors/is the graveyard until emptied). Map foreign errors at the boundary; never leak another layer's or feature's type. Translation is a presentation extension, not a method on the failure:presentation/<feature>_failure_l10n.dartexposestoTranslated(BuildContext)— the only placecontext.loc/fluttertouches a failure (keepsdomain/+data/Flutter-free); thesealedswitch makes a missing user message a compile error. The end user never sees a dev string: the catch-all variant returns a generic localized message (oopsSomethingWentWrong), never the rawlogMessage(logged at the boundary, for us only) —unexpected: (m) => mleaks dev detail. Propagation: repositories returnResult<T, F extends Failure>(variantsOk/Err, generic overFso consumers never cast,@useResult; helpersfold/map/mapErr);throwonly fordart:corebugs (Flutter Result). The repository is the onetry/catchboundary (or the feature use-case when wrapping a shared core repo that still throws); use-cases forward/composeResults; the blocswitches and holds the typed<Feature>Failurein state (noBuildContextinpresentation/logic); the UI rendersfailure.toTranslated(context). Migration (#1895) is sanctioned and staged — bring a feature fully in line when you touch it, one feature per PR. Legacy (don't replicate, converge per-feature):BullException+ hardcoded English;<Feature>Errornaming;toTranslatedon the error; per-layer splits (domain_errors.dart/application_errors.dart/…);fund_exchange's feature-prefixed singular. -
Acyclic feature graph. Check FEATURES.md before adding a dependency.
-
Keep the dependency graph live. Any PR that adds a feature, removes a feature, or changes which other features it consumes via
public/facades must update the mermaid graph in FEATURES.md in the same commit. The graph is documentation only if it matches the code — if you change one without the other, both become useless. -
Folders justify their existence — files don't justify folders. A tiny piece of code is one file with a role suffix, not a folder of one file:
- One use case →
<feature>/domain/<verb>_<noun>_usecase.dart. Don't createdomain/usecases/for a single file. - One entity →
<feature>/domain/<noun>.dart. Don't createdomain/entities/for a single file. - One datasource →
<feature>/data/<noun>_datasource.dart. Don't createdata/datasources/for a single file.
Create the folder the moment a second file of that kind appears (or you know it's imminent in the same PR). Don't pre-create empty folders or use
.gitkeep. Suffix carries the role; path carries the layer.Exception (melos migration):
packages/andfeatures/are intentionally pre-created with.gitkeepas reserved workspace homes for the in-progress monorepo migration — do not remove them or treat them as a rule-#14 violation. See the Monorepo / melos section. - One use case →
-
Enforce with the compiler, not hope. Repository contracts →
abstract interface class(forbidsextends, forcesimplements); lock finer capabilities with@Deprecated.implement()/.instantiate()/.extend()(Dart 3.10+). Failure families and multi-case states →sealed(a missingswitchcase is a compile error).Result-returning repo methods → annotate@useResult(package:meta) so a discarded result warns. Cross-feature anddata/-from-presentation/bans → a rule in the first-party analyzer plugin system (Dart 3.10+, the supported successor tocustom_lint); under melos, package boundaries + theimplementation_importslint make them hard errors. All available in the current toolchain (Flutter 3.41/3.44 bundle Dart 3.11/3.12) — no point-release dependency; nothing new in 3.41/3.44 is required.
When a request would break these rules, explain why and propose a compliant alternative. Don't silently comply.
When you work in a feature, file, or folder that already violates these rules (legacy, a drifted hexagonal module, a leaked boundary), don't silently work around it and don't quietly copy it. Flag it and offer a fix: name the specific rule it breaks and propose a scoped refactoring to bring it into line — as its own atomic commit/PR, separate from the task at hand. Then let the developer decide: take it now, defer it, or skip it. Never fold an opportunistic refactor into an unrelated change (that breaks atomic commits). This is the "leave it cleaner than you found it" rule from ARCHITECTURE.md's intro, kept compatible with atomic commits — surface the opportunity, don't force it.
Codified from a sweep of the actual codebase. ARCHITECTURE.md is silent on most of these; the rules below reflect what the dominant code already does. Where two conventions exist, the recommendation is the more common one — migrate toward it, don't replicate the minority.
Folders (inside a feature):
| Folder | Convention | Notes |
|---|---|---|
domain/entities/ |
plural | rich models + value objects |
domain/repositories/ |
plural | the single repositories/ folder — abstract interfaces (contracts) |
domain/usecases/ |
plural, no underscore | the canonical use-case home (~80% already here) |
data/datasources/ |
plural, no underscore | not data_sources/; one external system each |
data/models/ |
plural | wire/persistence model — 1 per entity, always separate from the entity |
data/mappers/ |
plural | model ↔ entity; a file only once it earns one (rule #14) |
data/ (repo impl) |
— | <noun>_repository_impl.dart lives directly here — no data/repositories/ folder |
presentation/ |
— | bloc/cubit + state + event |
ui/screens/, ui/widgets/ |
plural | |
public/ |
— | facade lives directly inside |
watchers/ |
plural, optional | drives use-cases, never repositories |
Deprecated — converge away (see ARCHITECTURE.md "Convergence"):
application/,adapters/,interface_adapters/,frameworks/.
Files — snake_case, singular suffix:
| Kind | Pattern | Example |
|---|---|---|
| Use case | <verb>_<noun>_usecase.dart |
broadcast_bitcoin_transaction_usecase.dart |
| Repository interface (abstract) | <noun>_repository.dart in domain/repositories/ |
bitcoin_wallet_repository.dart |
| Repository impl | <noun>_repository_impl.dart or <tech>_<noun>_repository.dart, in data/ |
exchange_rate_repository_impl.dart, drift_electrum_server_repository.dart |
| Datasource | <noun>_datasource.dart or <tech>_<noun>_datasource.dart, in data/datasources/ |
electrum_remote_datasource.dart, bdk_wallet_datasource.dart |
| Entity / value object | <noun>.dart |
wallet.dart, auto_swap.dart |
| Model (wire/persistence) | <noun>_model.dart in data/models/ |
wallet_utxo_model.dart |
| Mapper | <noun>_mapper.dart in data/mappers/ |
wallet_utxo_mapper.dart |
| Bloc | <feature>_bloc.dart |
send_bloc.dart |
| Cubit | <feature>_cubit.dart |
settings_cubit.dart |
| State | <feature>_state.dart |
send_state.dart |
| Event | <feature>_event.dart |
send_event.dart |
| Facade | <feature>_facade.dart |
labels_facade.dart |
| Locator | <feature>_locator.dart |
wallet_locator.dart |
| Failure (family) | <feature>_failure.dart (domain) + <feature>_failure_l10n.dart (presentation) |
per rule #11 |
Classes — PascalCase, matching local quirks:
- Use cases:
<Verb><Noun>Usecase— lowercase 'case', neverUseCase. Codebase-wide convention (GetSettingsUsecase,UpdateTorSettingsUsecase,BroadcastBitcoinTransactionUsecase). Single public entry methodexecute(...)(206 of 209 use-cases; nevercall). - Datasources:
<Name>Datasource— lowercase 's', neverDataSource. Codebase-wide (BdkWalletDatasource,BoltzDatasource). - Repositories: abstract interface =
<Name>Repository(abstract interface class, indomain/repositories/); concrete impl =<Name>RepositoryImplor<Tech><Name>Repository(indata/). Pick the technology prefix when the backing tech is meaningful (Drift, Bdk, Boltz, GoogleDrive, Bullbitcoin); useImplwhen the technology label would be awkward. - Entities & value objects: bare noun, no
Entitysuffix (Wallet,WalletUtxo,TransactionOutput). Two legacy outliers (BitboxDeviceEntity,LedgerDeviceEntity) — don't replicate. - Models (wire/persistence):
<Noun>Model(WalletUtxoModel) — indata/models/, freezed; serialization only, never crosses the repository boundary (rule #6). - Mappers:
<Noun>Mapper(WalletUtxoMapper) — indata/mappers/; translates model ↔ entity. - Non-repository domain interfaces:
<Name>Port(BlockchainPort,ElectrumConnectivityPort) — a capability abstraction that is not a repository; lives indomain/. Repository interfaces use<Name>Repository, neverPort. (ThePortsuffix stays for these; only the hexagonal foldersapplication//adapters//frameworks/are deprecated.) - Bloc/Cubit:
<Feature>Bloc/<Feature>Cubit(SendBloc,SettingsCubit). - State / Event:
<Feature>State/<Feature>Event, sealed with freezed when there are multiple cases. - Facade:
<Feature>Facade(LabelsFacade). - Watcher (when introduced):
<Subject>Watcher. - Failure: one sealed
<Feature>Failurefamily per feature indomain/<feature>_failure.dart(baseFailure); never<Feature>Error—Erroris reserved fordart:corebugs. Translation extension<Feature>FailureL10ninpresentation/. See rule #11.
When the same role exists with two names in the codebase, use the dominant one and flag the outlier as legacy. Don't rename outliers in unrelated PRs — that breaks atomic commits.
Inside a class, declare members in this order, with a blank line between each group:
- Instance fields — the object's state. Reading these first tells you what the thing is.
- Constructor(s) — how it's built, on top of the fields you just read.
- Methods — its behaviour, including
@overrides.
class Amount {
final BigInt sats;
Amount(this.sats) {
if (sats < BigInt.zero) throw ArgumentError('Amount cannot be negative');
}
bool get isDust => sats < BigInt.from(546);
}This is fields-first on purpose: our domain, value, and error types are defined by their state, so surfacing it first reads most naturally (and matches the field-first habit of most other languages). It is a soft convention — the analyzer does not enforce member order, so keep it consistent by hand and in review.
Do not enable the sort_constructors_first lint. It enforces the opposite order (constructor before fields); turning it on would fight this convention and churn every class. Widgets, whose constructor is effectively their public API, may keep the constructor first if that reads better there — don't reformat existing widgets to satisfy this rule.
The codebase is migrating toward a real UI Kit. The skeleton already lives in lib/core/widgets/ (buttons/, cards/, inputs/, lists/, dropdown/, bottom_sheet/, text/, selectors/, …) and theme tokens in lib/core/themes/. Today the same widgets get re-implemented per feature (_SaveButton, _ConfirmButton, _BottomButtons …) — stop adding to that pile.
The go-forward home for the kit is the bull_ui design-system package (packages/bull_ui; see ARCHITECTURE.md "Design-system package"). Provenance: BB* = legacy lib/core/widgets, Bull* = bull_ui. A feature whose UI is built on the kit imports only package:bull_ui/bull_ui.dart for widgets — never package:flutter/material.dart/cupertino.dart/widgets.dart (the coins feature is the first to enforce this). Migration of core/widgets into bull_ui is incremental (one scoped PR at a time), so both still exist; prefer a Bull* widget when one exists, otherwise follow the workflow below against lib/core/widgets/.
Workflow when you need a widget:
- Search
lib/core/widgets/first. If something close exists, use it. - If close but missing a variant, extend the core widget (new prop, new constructor) rather than fork. One source of truth, no copy-paste.
- If genuinely new and reused by ≥ 2 features, put it in
lib/core/widgets/<category>/from the start — that is growing the UI Kit. - If used by exactly one feature, it lives in
<feature>/ui/widgets/— but write it composable enough to be promoted later (no hardcoded colors, no hardcoded text, take callbacks not bloc refs). - Widgets never live under
adapters/,frameworks/,domain/, orapplication/. UI goes inui/orlib/core/widgets/. Full stop. - No hardcoded user-facing strings. Always
context.loc.<key>— theBuildContextextension (build_context_x.dart) that wrapsAppLocalizations.of(context); it is the dominant convention (≈2564 uses vs 3 rawAppLocalizations.of). Manage keys inlocalization/withtools/arb.dart(fvm dart run tools/arb.dart help) — don't hand-edit the.arbfiles — then runmake translations. A duplicated literal across screens means a missing l10n key. - Theme tokens only — colors, spacing, typography pulled from the theme. See rule #10 above.
When you spot a duplicate of an existing core widget in feature code, flag it in the PR description as a follow-up cleanup. Don't silently leave it. Don't fix unrelated duplicates in the same PR either — that breaks atomic commits.
Format: type(scope): description
- type:
feat | fix | refactor | chore | docs | test | ci | build | perf | style - scope: feature folder name (
wallet,send,payjoin,swap,core,ci,hooks, …) — lowercase, single noun. - description: imperative, lowercase, no trailing period, ≤72 chars total subject line. Human-friendly and compact — describe the why if non-obvious; the what is in the diff.
Rules:
- Atomic. One logical change per commit. A feature + its refactor = two commits. A fix + cleanup = two commits. Independently revertable.
- No
Co-Authored-Bytrailer. - No
--no-verify. Pre-commit must pass on its own. - Reference the recent history (
dc3642801,14cce2ad6,1fecb0bb8) for style.
- Use cases and entities with business rules must have unit tests.
- Layout: tests live in
test/mirroringlib/(test/features/<feature>/…, core undertest/core_test/); files end_test.dart. - Tooling:
bloc_testfor blocs/cubits,mocktail/mockitofor collaborators; prefer fakes for repositories/datasources — the abstract interfaces exist precisely so you can swap a real implementation for a test double. make unit-testfor the test you wrote. Don't claim done until it's green.- Integration tests live in
integration_test/and need device/.env fixtures.
- GitHub refs only, never
path:deps. All cross-repo packages are pinned viagit: { url, ref }(example). Path deps don't ship. - Adding a dep? Bump
pubspec.yaml, runmake deps, commitpubspec.lockin the same commit.
Your training data is stale; the ecosystem isn't. Flutter, Dart, BDK, Boltz, payjoin, freezed, drift, go_router, bloc — all churn fast. Pasting an API or library version from memory is the fastest way to waste the user's time.
Before recommending a library, API surface, syntax, flag, version, or pattern:
- Anchor on the current date. Run
date -u +%Y-%m-%dso you know what "recent" actually means in this session — your knowledge cutoff isn't today. - Cross-check at least one trustworthy source, freshness-matched to that date:
- Official docs for the library/framework (pub.dev for Dart packages, docs.flutter.dev, dart.dev).
- GitHub of the project itself —
gh release list,gh issue list -S "is:open <topic>", the actual source. Releases ≤12 months old preferred. - StackOverflow answers from the last ~2 years — older answers often describe deprecated APIs.
- Reddit (
r/FlutterDev,r/dartlang,r/programming) for community sentiment on patterns and recent breakage reports.
- Cite the source in chat or in the PR description: title + URL + a date stamp. "Per pub.dev/packages/foo (2026-04-15) …".
- If you can't verify, say so explicitly — "I think X but couldn't confirm" beats stating a wrong API confidently.
This applies equally to architectural suggestions, tooling, CI tricks, command flags, and security recommendations. Confidence ≠ correctness.
- Prefer
rtk <cmd>ifcommand -v rtksucceeds — token-optimized proxy. Falls back transparently if not installed. - Use
ghfor GitHub (PRs, issues, runs, releases). Ifghis unavailable, usecurlor Fetch against the REST API or fetch the page — never invent URLs. - Learn unknown CLIs with
--helpfirst. Don't guess flags.<cmd> --help,<cmd> subcommand --help, then act. - Get the current date with
date -u +%Y-%m-%dbefore judging anything as "recent" or "outdated" — see the verification section above.
This app holds users' keys. A leak is not a bug, it's a loss of funds. Hold these as hard as the architecture rules:
- Never log secrets. Mnemonics, seeds, xprivs, PINs, raw key material never reach logs, Sentry, or analytics. Scrub before reporting; assume anything logged is exfiltrated.
- Secrets are ephemeral. Read from
flutter_secure_storageat point of use; don't cache key material in long-lived bloc/singleton state. Treat a revealed value as short-lived. - Sealed UI for display. Show a secret through a widget that reads it internally and never returns it (the
MnemonicViewpattern — see ARCHITECTURE.md "Sealed UI as a security tool"); never add a getter that hands the raw value to a caller. - Block capture on secret screens —
no_screenshotplus exclusion from the semantics/accessibility tree. - Validate at the domain boundary. Addresses, amounts, descriptors are value objects that reject invalid input at construction (rule #9) — never trust a raw string deeper in.
- When a change touches key material, signing, or backup/recovery, say so explicitly in the PR so it gets the right review.
- Keep
SECURITY.mdcurrent, sourced, and reviewed. When a security question is settled — an external report is triaged, a protocol behavior is analyzed, a security-relevant dependency changes — document the outcome inSECURITY.md: every claim must cite an official source (BIP/RFC section, upstream repo/release, exact dependency version frompubspec.lock) and the change must get a careful review before merging. Future reports are then triaged against documented, verifiable invariants instead of re-analyzed from scratch.
- Don't run
flutter/dartwithoutfvm. - Don't run
flutter analyze <path>— whole project only. - Don't bypass
.git_hooks/pre-commit. - Don't import across feature internals — facade only.
- Don't put business logic in BLoCs or
/lib/core. - Don't log, cache, or expose secrets (mnemonic / seed / xpriv / PIN) — see Security.
- Don't return a
data/model from a repository, or a non-published type from a facade — boundaries carry domain types only. - Don't reinvent a widget that already exists in
lib/core/widgets/. - Don't hardcode user-facing strings — use
context.loc.<key>. - Don't hard-wrap prose mid-sentence in markdown, comments, PR descriptions, or commit bodies — write each sentence on one continuous line and let the editor soft-wrap. Manual line breaks belong only between paragraphs or list items.
- Don't add
path:deps to pubspec. - Don't amend or force-push without explicit ask.
- Don't add
Co-Authored-By. - Don't ship code you couldn't verify (
make unit-testred = not done). - Don't recommend an API, version, or pattern from memory — verify against pub.dev / GitHub / docs first.
Plan first, code second. Read the existing facade of the feature you're touching. If the architecture file says one thing and the code says another, the file wins — flag the legacy code as a follow-up rather than copying it.