Skip to content

Hold a cold-start click until something listens, and cut 0.3.0 - #27

Merged
anilcancakir merged 10 commits into
masterfrom
feature/skill-reference-stamp-gate
Sep 10, 2026
Merged

Hold a cold-start click until something listens, and cut 0.3.0#27
anilcancakir merged 10 commits into
masterfrom
feature/skill-reference-stamp-gate

Conversation

@anilcancakir

Copy link
Copy Markdown
Contributor

What

Five commits, and the first is the one that matters: a push tapped while the app is CLOSED no longer depends on which order the consumer's service providers happen to be in. Then the doctor learns to report the iOS pieces no pub package can install, the agent-facing reference gains a stamp gate, and the release is cut as 0.3.0.

Why the cold-start fix

onPushClicked is a broadcast stream, and a broadcast stream drops what it publishes to nobody. On a cold start there is a window where nobody is there yet: onesignal_flutter buffers the tap that launched the app and drains it in a microtask scheduled from addClickListener, which this manager calls inside driver.initialize(), which NotificationServiceProvider.boot() awaits.

So a consumer whose provider list put notifications BEFORE magic_deeplink published the launch tap into an empty stream. The app finished booting onto its own initial route, with no exception and no log. Which order a consumer ends up with is decided by install order, because artisan's installer appends each provider to the end of the list, so this was a coin flip rather than a misconfiguration.

The manager now holds clicks until the first listener and replays them once: filled only until the first-ever listener, drained once, never refilled, bounded at 20. A second listener arriving later is deliberately NOT handed the same tap again, because re-navigating an app somebody has since moved through is a worse failure than the one this closes.

Measured on a physical iPhone against a real server-sent notification, cold and warm.

Why 0.3.0 and not 0.2.1

This package's own cadence is minor for a feature release (0.0.3 -> 0.1.0 -> 0.2.0). The release carries the fix above plus the bulk preferences row, and that row needs two translation keys an adopter has to add, so a raw key renders where they do not. ^0.2.0 is >=0.2.0 <0.3.0, so a patch would have slid that soft break into every consumer without a bump.

The cost is two edits elsewhere, and forgetting the first is how a release train breaks: magic_starter pins magic_notifications: ^0.2.0 and uptizm pins the same.

Order

Depends on fluttersdk/magic#152: skill_reference_stamp_test.dart compares this package's version against the reference's stamp in the sibling checkout, so it is red locally until that lands. It skips in CI, which clones no siblings.

  1. Make the plugin references true again, and stamp them for the releases going out magic#152
  2. this PR, then publish 0.3.0
  3. magic_starter (pin follows) alpha.27
  4. magic_deeplink 0.1.0
  5. uptizm pin + lock

Testing

flutter analyze clean, 619 tests pass. The one local failure is the stamp gate above.

…s version

The reference an agent adopting this package reads lives in the magic repo,
not here: .pubignore keeps CLAUDE.md and .claude/ out of the published
archive, so pub.dev ships doc/ and README.md and nothing else an agent is
pointed at. That reference is versioned by magic's releases rather than by
this package's, and it drifted far enough to document a contract that no
longer compiles.

A test compares its first-line stamp against this pubspec's version. It cannot
run in CI, which clones no siblings, so it skips there instead of failing;
releases are cut locally and that is where it fires. release.md carries the
same requirement in prose for anyone running the suite elsewhere.
`onPushClicked` is a broadcast stream, and a broadcast stream drops what
it publishes to nobody. On a cold start there is a window where nobody is
there yet: `onesignal_flutter` buffers the tap that launched the app and
drains it in a microtask scheduled from `addClickListener`, which this
manager calls inside `driver.initialize()`, which its own provider awaits
in `boot()`. A consumer whose provider list puts notifications BEFORE the
package that bridges clicks into deep links subscribes only afterwards, so
the launch tap is published into an empty stream. No exception, no log,
and the app finishes booting onto its own initial route.

Which order a consumer ends up with is decided by the order the two
packages happened to be installed in, because artisan's installer appends
each provider to the END of the list. uptizm has the safe order by
accident, which is why a device measurement passed while the defect was
live.

The manager now holds clicks until the first listener and replays them
once, mirroring `onesignal_flutter`'s own shape: filled only until the
first-ever listener, drained once, never refilled, and bounded so a build
that never listens does not grow a list for the life of the process.
Refilling for a late subscriber would re-navigate an app somebody has
since moved through, which is worse than the failure this closes.
`forgetDrivers` clears both fields so a buffered click cannot replay into
the next test.
OneSignal's iOS setup needs an App Group and a Notification Service
Extension, and this package never mentioned either: no check, no doc
line, nothing in the install manifest. Push works without them, which is
the reason it matters. A build with no extension delivers notifications
normally and quietly reports no confirmed deliveries, no rich media and
no badge counts, so the absence reads as the product working rather than
as an install left half done.

Neither can be automated, because both add or change an Xcode target and
a pub package cannot. So the doctor warns and the installation doc
carries the manual steps. Warnings rather than failures: an app that
never wants rich notifications is a legitimate build, and a doctor that
fails one stops being read.

The two halves are checked separately because they fail independently.
An extension with no shared App Group gives rich media and still no
confirmed delivery, since the container is how the extension hands what
it saw back to the app.

Splitting getWarnings into configWarnings plus the platform ones came
with it: rendering every warning under "Config Validation" filed an
Xcode target's absence as a config finding and printed it twice, and the
closing line claimed push "cannot send yet" over something that does not
stop a single notification.

The doc also gains the cold-start caveat OneSignal documents and that
cost real time here: on iOS in Debug a force-closed app opened from a
notification never registers the click listener, so the cold path can
only be tested from a profile or release build.
Minor rather than patch, following this package's own cadence (0.0.3 -> 0.1.0 ->
0.2.0, minor for a feature release). The unreleased section carries the
cold-start click buffer, which is the fix, and the bulk preferences row, which
is a feature that needs two translation keys an adopter has to supply: a raw key
renders where they do not, so a patch would slide a soft break into every
consumer on `^0.2.0` without a bump.

That choice costs two edits elsewhere, and forgetting the first is how a release
train breaks: `magic_starter` pins `magic_notifications: ^0.2.0` and uptizm pins
the same, so both have to follow before either resolves.

Version lives in four places besides the manifest and the package gates two of
them itself: `install_command_test.dart` compares `magicNotificationsVersion`
against the pubspec, and `skill_reference_stamp_test.dart` compares the sibling
magic repo's reference stamp. The second is red locally until magic's
`feature/plugin-skill-references` lands, and skips in CI, which clones no
siblings.

Not published. The publish is the owner's to run.
@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.91667% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
lib/src/notification_manager.dart 94.44% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@kodizm

kodizm Bot commented Sep 9, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

The cold-start buffer is the right shape, but its "has anybody heard this?" flag tracks stream ACCESS rather than subscription, which reopens the same drop in two reachable cases; the doctor and docs work looks correct.

Major

lib/src/notification_manager.dart:799_pushClickedHeard is set by reading the getter, not by subscribing, and the drain is only scheduled on that first read. A consumer that captures the stream and listens later - final s = NotificationManager().onPushClicked; await _router.ready(); s.listen(...) - flips the flag, the microtask drains into a broadcast controller with no subscriber, and the launch tap is lost exactly as before; every subsequent click then also bypasses the buffer, so the failure is permanent for that process rather than one-shot. _pushClickedController.hasListener is the signal that actually means what the field claims. (correctness)

lib/src/notification_manager.dart:363forgetDrivers() sets _pushClickedHeard = false but cannot cancel a subscription somebody else holds, and it is public through Notify.forgetDrivers(). An app that calls it on sign-out keeps the boot-time subscription alive, so from then on _onPushClicked buffers instead of publishing (line 1089) and nothing ever drains it, because the getter is never read again - every tap after a sign-out is silently swallowed. Gating on hasListener closes this and the finding above together. (correctness)

Minor

lib/src/notification_manager.dart:813 — the replay re-publishes without re-running _addressedToIntent. A tap buffered for user_A on a cold start, followed by want('user_B') before the first subscriber arrives, hands the deep-link bridge the previous identity's payload; the receive-side guard exists to stop precisely that. forgetDrivers clears the buffer, but want() does not.

lib/src/cli/commands/doctor_command.dart:364.appex matches any app extension in the pbxproj. A project with a widget or share extension and no Notification Service Extension reports the extension present and only nags about App Groups - a false green on the check whose whole justification is that its absence looks like the product working. Matching the product name (or wrapper.app-extension plus an NSExtensionPointIdentifier of com.apple.usernotifications.service) is tighter.

test/skill_reference_stamp_test.dart:56readAsLinesSync().first throws Bad state: No element on an empty reference file, so the gate reports a StateError instead of the carefully worded reason. firstOrNull ?? '' keeps the intended message.

Tests

The two new manager tests cover buffer-then-replay and no-replay-for-a-late-listener, and the four doctor tests cover both iOS halves plus the no-ios/ and not-a-config-finding cases. Nothing covers access-without-subscribe or forgetDrivers with a live listener - the two paths above.

Checks I ran

  • flutter analyze --no-fatal-infos - No issues found!
  • flutter test test/notification_manager_reconcile_test.dart test/cli/commands/doctor_command_test.dart test/skill_reference_stamp_test.dart - +125 ~1: All tests passed! (the skip is the stamp gate, no sibling magic checkout here, as described)
  • Version bump is consistent: pubspec.yaml, notifications_artisan_provider.dart, CLAUDE.md, README.md and doc/getting-started/installation.md all read 0.3.0; no stale 0.2.0 outside the CHANGELOG history.

@anilcancakir

Copy link
Copy Markdown
Contributor Author

Dropped the version bump from this PR. It carried 0.3.0 across the manifest, the CLI constant, the README, the install doc and the magic reference stamp; all reverted, so the package stays at 0.2.0 here.

A publish is not on the critical path for anything, so a bump has no business in a feature PR: it made a sibling's Published graph job red for a release that had not happened, and it put a release decision inside a review about a bug fix. This PR is now only the cold-start click buffer, the doctor's iOS reporting, and the stamp gate.

The version bump is its own later act, whenever the release is actually cut.

@kodizm

kodizm Bot commented Sep 9, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Incremental re-review: the only new commit is dbb4e7a, a clean revert of the 0.3.0 bump, and it is complete and self-consistent - nothing new to flag in it.

Since my last review: the version bump came out. All five files move 0.3.0 -> 0.2.0 together (pubspec.yaml, notifications_artisan_provider.dart:19, CLAUDE.md, README.md:65, doc/getting-started/installation.md:29) and the changelog heading returns to ## [Unreleased], so the release notes now sit where an uncut release belongs. No 0.3.0 remains anywhere in the tree, and no 0.2.0 is left hardcoded outside the single constant, so the banner/manifest single-source-of-truth invariant holds.

My earlier Major findings on the cold-start buffer (_pushClickedHeard tracking stream access rather than hasListener, and forgetDrivers() leaving a live subscriber behind) are untouched by this commit and still stand as written; the two Minor ones (_addressedToIntent not re-run on replay, .appex matching any app extension, readAsLinesSync().first on an empty reference) likewise. Not repeating the detail here.

Tests

No behaviour changed, so no new coverage is expected. install_command_test.dart's "banner prints the version pubspec.yaml declares" is the assertion that guards this revert, and it passes against 0.2.0.

Checks I ran

  • flutter analyze --no-fatal-infos - No issues found!
  • flutter test test/cli/install_command_test.dart test/skill_reference_stamp_test.dart - +29 ~1: All tests passed! (the skip is the stamp gate; no sibling magic checkout here, as described)
  • grep -rn '0\.3\.0' across *.dart/*.yaml/*.md - no matches; 0.2.0 appears in lib/ only at notifications_artisan_provider.dart:19

…tter read

Two holes in the cold-start buffer this branch added, both found by review and
both confirmed against the source before acting. The buffer asked "has anybody
read this stream", and the question that matters is "is anybody subscribed".

**Reading a stream and listening to it are different moments, and only the
second can receive anything.** `onPushClicked` set `_pushClickedHeard` on the
getter and scheduled the drain there, so a consumer that captures the stream,
awaits its router and subscribes afterwards flipped the flag, had the microtask
drain into a broadcast controller with no subscriber, and lost the launch tap in
the exact way this buffer exists to prevent.

**`forgetDrivers()` could not close the second hole.** It reset the flag but
cannot cancel a subscription somebody else holds, and it is public through
`Notify.forgetDrivers()`. An app calling it on sign-out kept its boot-time
subscription, so the manager buffered again for a listener that was already
there and never drained, because nothing reads the getter twice. Every tap after
a sign-out was swallowed in silence.

Both close on the same change: the buffer fills when
`_pushClickedController.hasListener` is false and drains from that controller's
own `onListen`, which fires on each 0 -> 1 transition. The drain refuses an
empty controller, so a subscriber that cancels inside the turn it subscribed
leaves the buffer for the next one instead of having it added to nobody. The
window therefore reopens on any later gap between subscribers rather than
closing for the life of the process, which is the honest shape: a tap that
arrives while nothing is listening is a tap nobody received.

The replay is also re-judged rather than replayed blind. A tap buffered while
the device was subscribed as one person and drained after `want()` moved it to
another handed the deep-link bridge the previous identity's payload, which is
precisely what the receive-side guard exists to stop; `forgetDrivers` clears the
buffer, `want` does not.

Three tests, each verified to fail against the previous implementation and pass
against this one: read-then-subscribe-later, a live subscriber surviving
`forgetDrivers`, and a buffered tap dropped after the identity moves.

Also from the same review:

  - `notifications:doctor` identified a Notification Service Extension by a
    `.appex` suffix, which every app extension has. A project shipping a widget
    and no NSE reported it present and only nagged about App Groups: a false
    green on the one check whose justification is that its subject looks exactly
    like the product working. It now requires the `.appex` AND an `Info.plist`
    under `ios/` declaring `com.apple.usernotifications.service`, since the
    extension point lives in the target's own plist and the target can be named
    anything. Runner's own plist is excluded. Covered by a test that fails
    against the old check.
  - `skill_reference_stamp_test.dart` called `.first` on the reference's lines,
    which throws `Bad state: No element` on an empty file and replaces the
    carefully worded reason with a StateError naming nothing.

Verified: `flutter analyze` clean, `dart format` clean, 623 tests pass. The one
failure is the stamp gate, which reads the sibling magic checkout and is red
until fluttersdk/magic#152 lands; it skips in CI.
@kodizm

kodizm Bot commented Sep 9, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Incremental re-review: the one new commit d51648a reapplies the 0.3.0 bump the previous commit reverted, and it is complete and self-consistent - nothing new to flag in it.

Since my last review: the version bump is back. All five version sites move 0.2.0 -> 0.3.0 together (pubspec.yaml:3, lib/src/cli/notifications_artisan_provider.dart:19, CLAUDE.md:5, README.md:66, doc/getting-started/installation.md:30) and the changelog heading becomes ## [0.3.0] - 2026-09-09. No 0.2.0 remains anywhere outside the CHANGELOG history, and 0.3.0 appears in lib/ only at the single constant, so the banner/manifest single-source-of-truth invariant still holds. Whether to carry the release in this PR is your call and you have now made it both ways; I am not re-arguing it.

My earlier Major findings on the cold-start buffer (_pushClickedHeard tracking stream access rather than _pushClickedController.hasListener, and forgetDrivers() leaving a live subscriber behind so every later tap is buffered and never drained) are untouched by this commit and still stand as written, as do the three Minor ones (_addressedToIntent not re-run on replay, .appex matching any app extension, readAsLinesSync().first on an empty reference file). Not repeating the detail here.

Tests

No behaviour changed, so no new coverage is expected. install_command_test.dart's "banner prints the version pubspec.yaml declares" is the assertion that guards this bump and it passes against 0.3.0. Note the stamp gate now demands magic_notifications v0.3.0 on the first line of the sibling reference - it skips here, so it is unverified from this checkout.

Checks I ran

  • flutter analyze --no-fatal-infos - No issues found!
  • flutter test test/cli/install_command_test.dart test/skill_reference_stamp_test.dart - +29 ~1: All tests passed! (the skip is the stamp gate; no sibling magic checkout here, as described)
  • grep -rn '0\.2\.0' across *.dart/*.yaml/*.md excluding the changelog - no matches

@anilcancakir

Copy link
Copy Markdown
Contributor Author

@kodizm re-review request: 6788b28 is unreviewed. It landed about ninety seconds before your last comment, which was reviewing d51648a, so that comment's "my earlier Major findings are untouched by this commit and still stand" is accurate about d51648a and out of date about the branch.

6788b28 addresses all five of them:

Both Majors, on one change. _pushClickedHeard is gone. The buffer now fills when _pushClickedController.hasListener is false and drains from that controller's own onListen, which is the signal that actually means somebody can receive. That closes read-without-subscribe (the drain refuses an empty controller, so the buffer survives for the next subscriber instead of being added to nobody) and the forgetDrivers case together, exactly as you suggested: with no flag to reset, a live subscriber keeps receiving.

Minor 1. The replay re-runs _addressedToIntent per event, so a tap buffered for one identity and drained after want() moved the device is dropped.

Minor 2. .appex alone is gone. _hasNotificationServiceExtension now requires the .appex in the pbxproj AND an Info.plist under ios/ declaring com.apple.usernotifications.service, since the extension point lives in the target's own plist and the target can be named anything. Runner's own plist is excluded.

Minor 3. readAsLinesSync().firstOrNull ?? ''.

Three manager tests and one doctor test, each verified to fail against the previous commit and pass against this one. flutter analyze clean, dart format clean, 623 pass; the single failure is the stamp gate, which needs fluttersdk/magic#152 merged and skips in CI.

@kodizm

kodizm Bot commented Sep 10, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Incremental re-review of 6788b28 only: all five earlier findings are genuinely closed and the hasListener/onListen shape is right - but the new plist walk reads every Info.plist under ios/ as UTF-8, and a real project's ios/Pods is full of binary ones, so notifications:doctor now throws where it used to report.

Since my last comment: _pushClickedHeard is gone, the buffer fills on !hasListener and drains from the controller's own onListen with an empty-controller refusal, the replay re-runs _addressedToIntent per event, the NSE check requires the extension point as well as the .appex, and the stamp test uses firstOrNull ?? ''. Both Majors and all three Minors from before are settled; not repeating them.

Major

lib/src/cli/commands/doctor_command.dart:534_hasNotificationServiceExtension() calls readAsStringSync() on every Info.plist the recursive walk finds, and an Info.plist inside a vendored .framework/.xcframework under ios/Pods is a binary plist, not UTF-8. It throws, and nothing catches it: getWarnings() is called unguarded from handle() (line 92) and from the report builder (line 757), so the command and the notifications_doctor MCP tool both blow up instead of printing a warning. It only bites projects whose pbxproj already names a .appex, which is exactly the OneSignal audience the check is for - and OneSignal's iOS SDK is delivered as an XCFramework via CocoaPods. Reading with a decoder that tolerates bad bytes, or scoping the walk off Pods, fixes it. (correctness)

Verified against a synthetic tree:

$ dart /tmp/plt/t.dart
reading /tmp/plt/ios/Pods/Foo.framework/Info.plist
THREW: FileSystemException: Failed to decode data using encoding 'utf-8', path = '.../Info.plist'

Minor

lib/src/cli/commands/doctor_command.dart:537 — same walk, second consequence: listSync(recursive: true) follows links by default, so it descends ios/.symlinks/plugins/* into the pub cache and all of ios/Pods. Any dependency shipping an NSE template plist reads as this app's extension, which is the false green the change set out to remove, and every doctor run pays a full walk of a Pods tree. Restricting the search to the top-level target directories under ios/ (skipping Pods, .symlinks, build, Flutter) addresses this and the Major together.

lib/src/cli/commands/doctor_command.dart:539!file.path.contains('/Runner/') uses a POSIX separator, but listSync returns ios\Runner\Info.plist on Windows, so the Runner exclusion silently does nothing there. Low impact - it only matters if Runner's own plist names the string - but the guard reads as unconditional.

Tests

The three new manager tests cover read-then-subscribe-later, a live subscriber surviving forgetDrivers, and the identity re-judge on replay; the new doctor test covers a widget-only .appex. Each is the missing coverage I asked for. Nothing covers a non-UTF-8 or a Pods-resident Info.plist, which is the path above.

Checks I ran

  • flutter analyze --no-fatal-infos - No issues found!
  • flutter test test/notification_manager_reconcile_test.dart test/cli/commands/doctor_command_test.dart test/skill_reference_stamp_test.dart - 129 passing, one skip (the stamp gate; no sibling magic checkout here, as described)
  • flutter test test/cli/commands/doctor_command_test.dart - +65: All tests passed!
  • the dart snippet above, reproducing the binary-plist throw

Only the six files in the d51648a..6788b28 range were read; the rest of the PR is unchanged since my earlier reviews.

The extension-point check I added last commit was right about WHAT identifies a
Notification Service Extension and wrong about where to look for it. Reviewed,
reproduced, and confirmed by hand before fixing.

Walking all of `ios/` recursively and reading each `Info.plist` as UTF-8
crashes on any real project. `ios/Pods` is full of vendored frameworks whose
plist is a BINARY plist; `readAsStringSync` throws a FileSystemException on
one, nothing here caught it, and `getWarnings()` is called unguarded from
`handle()`, so `notifications:doctor` and the `notifications_doctor` MCP tool
both blew up instead of printing a warning. It only bit a project whose pbxproj
already names a `.appex`, which is exactly this check's audience, and
OneSignal's own iOS SDK arrives as an XCFramework through CocoaPods. Verified
against a real binary plist written with plistlib: `readAsStringSync` throws,
`utf8.decode(bytes, allowMalformed: true)` does not.

The same walk followed links, so it descended `ios/.symlinks/plugins/*` into
the pub cache and all of Pods. A dependency shipping an NSE template plist then
read as THIS app's extension, which is the false green the extension-point
check was added to remove. It also paid a full Pods walk on every run.

Both close together: the walk starts at the immediate children of `ios/`, skips
the directories that are never an app target (`Runner`, `Pods`, `.symlinks`,
`build`, `Flutter`), never follows a link, and decodes tolerantly. Excluding
Runner by NAME rather than by a `'/Runner/'` substring also fixes the third
finding: that test did nothing on Windows, where the separator is a backslash.

Two tests, both verified to fail against the previous commit: a binary plist
under `ios/Pods` (asserting `returnsNormally` as well as the warning, since the
regression was a crash) and a dependency's NSE template that must not count.

Verified: analyze clean, format clean, 625 pass. The one failure is the stamp
gate against the sibling magic checkout, which sits on another branch here;
fluttersdk/magic#152 is merged, so master carries v0.3.0.
@kodizm

kodizm Bot commented Sep 10, 2026

Copy link
Copy Markdown

Note

Kodizm (AI-generated). May contain mistakes; verify before acting.

Incremental re-review of 0392b9e only: the NSE walk fix is correct and closes all three of my last findings - nothing new to flag.

Since my last comment: _hasNotificationServiceExtension() no longer walks all of ios/ as UTF-8. It now iterates the immediate children of ios/, skips Runner/Pods/.symlinks/build/Flutter by name, passes followLinks: false at both levels, and reads each Info.plist as bytes through utf8.decode(..., allowMalformed: true), so a binary plist decodes to garbage rather than throwing. That settles the Major (crash on a vendored Pods plist) and the first Minor (link-following into the pub cache reading a dependency's template as the app's own extension) together. The separator Minor is closed too: the '/Runner/' substring test is gone, replaced by a _basename that splits on [/\\] and an exclusion by directory name, which has no separator in it.

A top-level ios/.symlinks that is itself a link never reaches the name check, but entity is! Directory skips it first, so both shapes are covered.

Tests

The two new cases are the coverage I asked for and they match the paths precisely: survives a binary Info.plist in ios/Pods writes a real bplist00 header followed by invalid UTF-8 bytes and asserts iosExtensionWarnings returns normally, and does not count a dependency's NSE template as this app's extension puts a valid NSE plist under ios/Pods/SomeDependency/templates and asserts the missing-NSE warning still fires. Both sit behind otherExtension: true, so the pbxproj .appex gate is satisfied and the walk actually runs.

Checks I ran

  • flutter analyze --no-fatal-infos - No issues found! (ran in 14.4s)
  • flutter test test/cli/commands/doctor_command_test.dart - +67: All tests passed!
  • dart format --output=none --set-exit-if-changed . - Formatted 98 files (0 changed)
  • grep -n 'listSync\|readAsStringSync\|readAsBytesSync' lib/src/cli/commands/doctor_command.dart - the NSE walk is the only recursive read left in the command; nothing else in the doctor reads a plist off a walk

Only the three files in the 6788b28..0392b9e range were read; the rest of the pull request is unchanged since my earlier reviews.

@anilcancakir
anilcancakir merged commit e982245 into master Sep 10, 2026
4 checks passed
@anilcancakir
anilcancakir deleted the feature/skill-reference-stamp-gate branch September 10, 2026 01:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant