Review Skill
SkillMediaReview all uncommitted changes before pushing. Checks for dead code, stale comments, AGENTS.md rule violations, unused imports, and inconsistencies introduced during the current session. Invoke with $review-before-commit.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Review Skill skill
What this skill tells your AI
The instructions your AI receives, as published by divinevideo/divine-mobile in .agents/skills/review-before-commit/SKILL.md and read by ahel’s review.
Purpose
Final code review of all uncommitted changes before pushing. Catches issues that are easy to introduce during iterative development: stale comments referencing removed code, dead code, rule violations, and inconsistencies.
How to Review
Step 1: Identify changed files
Run git diff --name-only and git diff --cached --name-only to get the full
list of modified files (staged + unstaged). If the user provided a path argument,
filter to only files under that path.
Step 2: Read all changed files
Read every changed file in full. For each file, check the items below.
Step 3: Run checks
For each changed file, check for:
Dead Code
- Unused imports (import not referenced anywhere in the file)
- Unused private fields, methods, or getters
- Unreachable code after early returns
- Commented-out code blocks (should be deleted, not commented)
Stale References
- Comments or doc strings referencing methods, classes, or variables that no longer exist in the codebase (use Grep to verify references are still valid)
- ABOUTME comments that no longer accurately describe the file
- TODO comments for work that was already completed in this session
Brainstorm Docs
Any file under mobile/docs/brainstorm/ (typically dated like
YYYY-MM-DD-issueNNNN-…-brainstorm.md) is a working artefact, not a
deliverable. By the time the PR is ready, the rationale belongs in the
PR description; the file should not ship with the merge commit.
Flag every staged file under that path and ask the user whether the brainstorm should be dropped or converted in place:
- Drop it (default) —
git rmthe file. Use a separatedocs: drop … brainstorm doccommit so the deletion is auditable. - Convert it to a decision record — only when the doc has lasting
value beyond the PR (e.g. captures a rejected approach future readers
will keep proposing). Rename, move out of
brainstorm/, and trim to the durable bits. Precedent:cd723075e docs(notifications): convert badge-desync brainstorm to decision record.
Precedents for outright deletion: 380bf50c1 docs: drop PR #4229 brainstorm doc, and the follow-up to PR #4234 review.
AGENTS.md Rule Violations
Read and apply ALL rules from AGENTS.md and .claude/rules/. Do not
hardcode specific rules here — always check the source of truth in those files.
Flutter Patterns — Known Review Findings
These patterns emerged from past review cycles on this repo and are not in the always-loaded rules (to keep the global context window lean). Check for them explicitly during code review.
Design System
-
Bespoke widget diverges from
divine_uiwithout a docstring note. Any widget that deliberately differs from adivine_uicomponent (different size, different structure, bypassed variant) must say why in its class docstring — which design-system component it's close to and what forced the divergence. Without it, reviewers will re-raise "why not just useDivineIconButton?"// Good /// Visually equivalent to a [DivineIconButton] in ghost style but sized 64×64 /// with a 32 px icon instead of DivineIconButton's 40×40/56×56 presets, /// because the Figma spec (node 15314:53971) calls for a larger tap target /// than any standard DivineIconButton size. class CenterPlaybackControl extends StatelessWidget { ... }
Widget API Design
-
Speculative parameters on reusable widgets. Before adding a parameter to anything in
divine_uiorlib/utils/, confirm the branch it unlocks is reachable from at least one caller. ABuilder/Callback/Resolverparameter with exactly one caller that always supplies it is a dead branch. Delete it — adding it back later is cheap; dead branches confuse readers and inflate the API surface.// Bad — initialChildSizeBuilder is only called from a surface where // the keyboard is never open, so the branch is unreachable. static Future<T?> show<T>({ double initialChildSize = 0.6, double Function(BuildContext)? initialChildSizeBuilder, }) { ... } // Good — speculative branch deleted. static Future<T?> show<T>({double initialChildSize = 0.6}) { ... } -
Ancestor injection via a one-off builder closure. When a call site needs to put a
BlocProvider/InheritedWidgetabove every slot of a modal/sheet/route, add acontentWrapperparameter to the target instead of passing aWidget Function(BuildContext, Widget)closure at the call site. The closure is a_buildFooin disguise and silently misses new slots added later.// Bad — builder closure baked into the call site. showMySheet(context, builder: (ctx, child) => BlocProvider<MyBloc>( create: (_) => MyBloc(), child: child)); // Good — target exposes contentWrapper; provider covers every slot. showMySheet(context, contentWrapper: (ctx, child) => BlocProvider<MyBloc>( create: (_) => MyBloc(), child: child));
State Management
-
**Modal-scoped bloc instantiated at the call site with
try/finally close(). ** Put theBlocProviderinside the modal's subtree (viacontentWrapperor equivalent).BlocProviderhandles the close automatically on unmount — any route that pops via a path other thanawaitreturning silently leaks the bloc in thetry/finallypattern.// Bad — lifecycle managed at call site. final bloc = CommentsBloc()..add(const CommentsLoadRequested()); try { await showSheet(title: BlocProvider.value(value: bloc, child: _Title())); } finally { await bloc.close(); } // Good — BlocProvider owns the close on unmount. return showSheet( contentWrapper: (ctx, child) => BlocProvider<CommentsBloc>( create: (_) => CommentsBloc()..add(const CommentsLoadRequested()), child: child), title: _Title());
Comments
-
Multi-line design-rationale comments for Codex's benefit. Paragraph- length inline comments drift the moment the code changes, and LLMs cite them as authoritative even when stale. If the explanation is more than one sentence, move it to the PR description or a rule file and leave a
// See PR #Npointer inline — not the full paragraph.// Bad — paragraph above ClipRRect that will silently drift. // The outer shell uses 30 px bottom corners and the inner tabs container // uses 32 px top corners so that the inner surface visibly nests ... ClipRRect(borderRadius: BorderRadius.vertical(bottom: Radius.circular(30))); // Good — rationale lives in the named constant. // Inner radius is 2 px larger so the tabs container visibly nests. ClipRRect(borderRadius: BorderRadius.vertical( bottom: Radius.circular(VineTheme.shellCornerRadius)));
UI / Animations
-
Redundant
ValueKeyonAnimatedSwitcherbranches that are already different runtime types.AnimatedSwitchercomparesruntimeType + key. Branches that already return different widget types (Center,IgnorePointer,SizedBox, …) are already distinguishable — addingValueKeyis redundant and risks "duplicate GlobalKey" assertions when the widget is reused. Only add aValueKeywhen (a) two branches are the same runtime type, or (b) a test anchors on the key viafind.byKey(...)(leave a comment saying so). -
Timer(orFuture.delayed) for UI timing.Timerkeeps firing after the widget is disposed unless manually cancelled; it doesn't pause with the route; it doesn't respectMediaQuery.disableAnimations; and it can land asetStatemid-frame. UseAnimationController+FadeTransition/AnimatedBuilderfor transient flashes, badge pulses, or snackbar-like overlays. Reach forTimeronly when the effect is genuinely outside the rendering pipeline (e.g. network debounce, analytics delay).// Good late final AnimationController _feedbackController = AnimationController( vsync: this, duration: const Duration(milliseconds: 550)); void _triggerFeedback() => _feedbackController.forward(from: 0); // In build: FadeTransition( opacity: Tween(begin: 1.0, end: 0.0).animate(_feedbackController), child: const FeedbackIcon());
Tests
-
Absolute wall-clock timing bounds (
<100 ns,<100 ms). Shared CI runners are noisy and substantially slower than dev laptops — these flake. Use a relative comparison (expect(fastMs, lessThan(slowMs))), wrap infakeAsyncfor logical-duration checks, or skip withskip: true+ aTODO(any):comment. Never bump the threshold — that just delays the next flake. -
expect(tester.takeException(), isNotNull)as a side-effect signal.very_good test --optimizationmerges test files; leaked RiverpodkeepAlivestate from an earlier test can silently resolve the dependency, suppressing the exception. Assert only the test's named contract; drain incidental errors without an assertion:tester.takeException(); // drain incidental error — no assertion -
tester.tapAt(Offset(...))for modal interaction. Coordinate-based taps are sensitive to the modal'sexpandflag:DraggableScrollableSheet(expand: false)has transparent empty space above its content, whileexpand: truefills that rectangle with the sheet's hit-testing. Preferfind.text(...)/find.byType(...)/find.bySemanticsIdentifier(...). WhentapAtis unavoidable, document the layout assumption inline and keep the offset well inside the intended region. -
DateTime.now()in code compared againsttester.pump(Duration). The Flutter test clock advances viapump;DateTime.now()reads the host wall clock — they don't agree. Usepackage:clock'sclock.now()in production code andwithClock(Clock(() => now), () async { ... })in the test so both clocks advance together. (clockis already a transitive dependency — nopubspec.yamlchange needed.)// Code under test: import 'package:clock/clock.dart'; if (clock.now().difference(_pausedAt!) >= _minPauseForFeedback) { _triggerUnpauseFeedback(); } // Test: var now = DateTime(2026); await withClock(Clock(() => now), () async { now = now.add(const Duration(milliseconds: 220)); await tester.pump(const Duration(milliseconds: 220)); expect(find.byType(UnpauseFeedback), findsOneWidget); });
Test Consistency
- If production code changed, verify corresponding tests exist and still match
- Check for test assertions that reference removed fields or methods
- Verify mock setups match current method signatures
Step 4: Run analyzer and tests
- Run
mcp__dart__analyze_fileson all changed files - Run
mcp__dart__run_testson all changed test files - Report any failures
Step 5: Report findings
Present findings grouped by severity:
## Review Summary
### Must Fix
- [file.dart:42](path/to/file.dart#L42) - Description of the issue
### Should Fix
- [file.dart:15](path/to/file.dart#L15) - Description of the issue
### Nitpick
- [file.dart:7](path/to/file.dart#L7) - Description of the issue
### All Clear
If no issues found, confirm: "No issues found. Ready to push."
Severity definitions:
- Must Fix: Will cause bugs, test failures, or CI failures
- Should Fix: Violates project rules, dead code, stale comments
- Nitpick: Style preferences, minor improvements
After reporting, fix all "Must Fix" and "Should Fix" items automatically. Ask the user before fixing "Nitpick" items.
Signals
- GitHub stars
- 264
- Forks
- 55
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
review-before-commit- Source
- github.com/divinevideo/divine-mobile