fluttersdk_telescope 0.0.5
fluttersdk_telescope: ^0.0.5 copied to clipboard
Passive runtime inspector for Flutter. Captures HTTP, logs, exceptions, DB queries, and Magic events via VM Service extensions. CLI tail and MCP tools for Claude Code.
Changelog #
All notable changes to this project will be documented in this file.
This project follows Semantic Versioning 2.0.0. Entries follow the Keep a Changelog shape.
[Unreleased] #
0.0.5 - 2026-08-25 #
Added #
-
A tenth ring buffer,
FramePerfRecord, for per-frame performance data. Carries the fieldsFrameTimingexposes (buildMicros,rasterMicros,vsyncOverheadMicros,totalSpanMicros) plus a per-frame block-attribution map.TelescopeStore.recordFramePerf/.recentFramePerf/.onFramePerfRecordfollow the shape of the existing nine buffers, with one deliberate difference: the buffer reads its ownsetFramePerfCapacity(default 3600, about a minute at 60fps) instead of the sharedsetCapacity, so a useful frame window does not simultaneously inflate the HTTP, log, exception, dump, model, cache, event, gate and query buffers.clearFramePerf()is a new public, non-test-only method that empties only this buffer, for a production caller that needs to zero it at the start of a measurement session without touching the other nine. (lib/src/records/frame_perf_record.dart,lib/src/telescope_store.dart,lib/telescope.dart) -
FramePerfWatcher, which fills that buffer by joining the two sources the engine reports separately. Frame magnitude comes fromSchedulerBinding.addTimingsCallback, which carriesFrameTiming.frameNumberbut arrives late and batched (roughly 100ms later on web). Per-frame attribution comes from drainingFlutterTimelineat the end of every frame, which knows what ran but not which frame number it was. So the drain parks its block map in a bounded pending map and the timings callback is the sole emission point, writing one complete record per timing. A timing with no block map still emits, because frame magnitude without attribution is still worth having; a block map whose timing never arrives is dropped by the bound.Two things about the drain are easy to get wrong and are pinned in the source. It re-registers itself as its last act, because
addPostFrameCallbackis one-shot and a drain that does not would run exactly once, freezing the liveness counter at 1 and making every later session unreportable. And the collect runs in a MICROTASK scheduled by the post-frame callback rather than inline in it:SchedulerBinding.handleDrawFramewraps the whole post-frame phase in its ownPOST_FRAMEspan, so an inline collect runs with a non-empty nesting stack and tripsassert(_stackPointer == 0)inFlutterTimeline. That assert is stripped outside debug, where the same call would instead write a stale start time into the freshly swapped buffer and report an enormous duration for a block that never ran.The watcher also exposes a static, monotonic
livenessCounterincremented once per frame drawn. It is the only reliable proof the engine is rendering:SchedulerBinding.framesEnabledwas measured reportingtrue, with aresumedlifecycle and an armedonReportTimings, on a Chrome page that had produced one frame in two seconds. The watcher is opt-in and is NOT auto-installed byTelescopePlugin.install(); register it withTelescopePlugin.registerWatcher(FramePerfWatcher()). (lib/src/watchers/frame_perf_watcher.dart,lib/telescope.dart) -
ext.telescope.frames,telescope:framesandtelescope_frames, the wire surface for the new frame-perf buffer. The extension returns the buffer's records alongsideFramePerfWatcher.livenessCounter, so a caller reading an empty result can tell a quiet app from a stalled engine without a second round trip. The naming follows telescope's plural-noun convention (requests,exceptions,queries, ...); the wire saysframes, notframe_perf, even though the Dart types keep that prefix to name the record and the watcher.TelescopeArtisanProvidernow ships 7 CLI commands and 10 MCP tools. (lib/src/extensions/register_telescope_extensions.dart,lib/src/commands/telescope_frames_command.dart,lib/src/telescope_artisan_provider.dart,test/src/commands/telescope_frames_command_test.dart)
Changed #
- The registry dispatch fires on a published release now, not on every push that touches the skill. Under the push trigger
fluttersdk/aiclimbed to v1.3.75, and most of those releases re-published identical skill content: a docs commit and a release commit each cost the registry a version. The registry version now tracks published telescope releases instead of counting commits.workflow_dispatchstays as the manual escape hatch when a skill fix has to reach users before the next release. (.github/workflows/dispatch-to-registry.yml)
Fixed #
-
The registry dispatch could never fire, so a release would have shipped a skill the registry never received.
dispatch-to-registry.ymldeclaredrelease: [published], butpublish.ymlcreates the release withsoftprops/action-gh-releaseunder the defaultGITHUB_TOKEN, and GitHub does not start workflow runs from events raised by that token. The run history confirms it: noreleaseevent has ever appeared there, only the manual run and the retiredpushtrigger. It has also had no chance to be noticed, since the switch to that trigger landed after the last release (0.0.4, 2026-06-17). The failure mode is silent, becausepublish.ymlgoes green either way and the only symptom is a user installing a skill one release behind.publish.ymlnow calls the workflow directly withneeds: github-release, which removes the cross-workflow event and sequences the dispatch after pub.dev has accepted the release rather than alongside it;workflow_callreplaces the dead trigger andworkflow_dispatchstays as the manual escape hatch. Secrets are passed by name rather than inherited, since the called workflow needs exactly two. Second bug in the same file, fixed alongside: the version extractor tested the ref against^v[0-9]+\.[0-9]+\.[0-9]+, but this repo tags without thevprefix, so that branch never matched a real tag and always fell through to readingpubspec.yaml. (.github/workflows/dispatch-to-registry.yml,.github/workflows/publish.yml) -
doc/mcp/tool-reference.mdstill promised newest-first records and a 200-entry cap. 0.0.3 corrected the MCP tool descriptions to the real wire shape and retired the newest-first shorthand, but this page kept it in all eightlimitrows, plus a "typically 200-500 depending on buffer type" capacity note.limitreturns the most recent N records in chronological order (_trimtakeslist.sublist(list.length - limit), so the oldest of that window is first and the newest is last), and every buffer caps at 500. A client reading this page and not reversing its iteration displayed the window backwards. (doc/mcp/tool-reference.md) -
telescope_tail's MCP schema told agents the log buffer holds 200 entries; it holds 500. Thelimitparameter description read "enforced by the ring-buffer size, typically 200" whileTelescopeStore._capis 500 and every other tool descriptor (telescope_requests,telescope_exceptions,telescope_events,telescope_gates,telescope_queries) correctly said 500. An agent budgeting a tail window against the documented number would under-read by 300 entries and conclude records had been evicted when they were still in the buffer. (lib/src/telescope_artisan_provider.dart) -
CI could not have passed on its next run, and
publish.ymlwould have blocked the release with it. The Flutter tool ships ananalysis_options.yamlmigrator that appends ananalyzer.excludeblock forbuild/plus the six platform runner directories, and it runs on everyflutter pub get. That is the first step of bothci.ymlandpublish.yml, so by the timedart pub publish --dry-runran later in the same job the checkout was dirty:1 checked-in file is modified in git, exit 65. Reproduced on a clean checkout ofmasterwith the exit code read directly rather than through a pipe. Nothing had reported it because no CI run here postdates the migrator. Both files now carry the block the migrator wants, which makes it a no-op; the hand-written comment above it survives apub get, since the migrator reads the parsed YAML, finds the excludes present and skips the file. Reverting the file inside the workflow was rejected as the alternative: it hides the drift and leaves every contributor's tree dirty after apub get. The same fix landed influttersdk_wind(#178) andfluttersdk_wind_diagnostics_contracts(#1). (analysis_options.yaml,example/analysis_options.yaml)
0.0.4 - 2026-06-17 #
Changed #
fluttersdk_artisanconstraint bumped^0.0.6->^0.0.8. Required for co-installability withfluttersdk_dusk0.0.7, which declaresfluttersdk_artisan: ^0.0.8. Without this bump, a downstream package listing bothfluttersdk_dusk: ^0.0.7andfluttersdk_telescopewould fail pub dependency resolution. No public API change; constraint only.telescope:installnow injectsimport 'package:magic_devtools/telescope.dart';and gates the Magic-stack wiring on themagic_devtoolsdependency instead of the removedpackage:magic/telescope_integration.dart. Coordinated with themagic_devtoolsextraction that movedMagicTelescopeIntegration(plus the 5 Magic watchers andMagicHttpFacadeAdapter) out of the magic core package. The injectedMagicTelescopeIntegration.install()call and all other wiring are unchanged.
Fixed #
telescope:installno longer injects themagic_devtoolsimport whenlib/main.darthas noawait Magic.init(anchor. Previously, the magic-side wiring block fired for any project that listedmagic_devtoolsin pubspec regardless of whether the app calledMagic.init. On a vanilla Flutter app this left an unused import that brokedart analyzein the consumer. The block is now gated onhasMagicInit && _hasMagicDevtoolsDep()so the import andMagicTelescopeIntegration.install()call are only injected for Magic-stack apps that actually callMagic.init. The existing try/catch aroundinjectAfterMagicInitis retained as a defensive backstop.
Documentation #
- Synced docs, skill files,
README.md, andCLAUDE.mdto themagic_devtoolsextraction and thefluttersdk_artisan ^0.0.8bump. The Magic-stack telescope adapter is now documented as shipping inmagic_devtools(imported viapackage:magic_devtools/telescope.dart, added as a dev_dependency); the installation / quickstart / watchers pages, the MCP setup snippet, and the skill (SKILL.md+ references) reflect themagic_devtoolsdependency gate and import. Dependency-version pins bumped from^0.0.3to^0.0.4acrossREADME.md,doc/getting-started/installation.md,doc/getting-started/quickstart.md, anddoc/mcp/setup.md; skill version stamp bumped to 0.0.4.
0.0.3 - 2026-05-28 #
Added #
- Skill v0.0.3: new
## 8. Community: star + issue (optional, once per session)section inskills/fluttersdk-telescope/SKILL.mdplus a newskills/fluttersdk-telescope/references/community.mdreference page. Trigger split: star CTA fires after the user confirms a telescope task end-to-end (captured HTTP record after a gesture, level-filtered tail slice, surfaced uncaught exception,clear-then-repro delta, or cleantelescope:install); issue CTA fires only on a genuine telescope-side bug (malformed MCP envelope,kInvalidParamsfor documented params,TelescopeStorelosing entries before the 500-cap,clearreturning anything but{"cleared": true}, shipped watchers throwing on a clean install,telescope:installexiting non-zero on a fresh consumer, orregisterExtensionIdempotentviolating idempotency). Issue CTA explicitly excludes the documented wired-but-empty buffers, swallowedtry / catchinvisibility, consumer-app exceptions, rawdart:io HttpClienttraffic gaps, the missingtelescope_modelsMCP tool, and FIFO eviction past 500. Preflight gates onghpresence and auth; failure prints the URL only, noopen/xdg-open/start. Both CTAs are prose-permission (notAskUserQuestion), maximum one star and one issue per session, declining one suppresses only that CTA. Labels: onlybugis applied (theagent-reportedlabel does not exist onfluttersdk/telescope, drop the flag). - Repo flow adopted GitHub Flow (single long-lived
master; retired thedevelopaccumulator).CLAUDE.mdand.github/copilot-instructions.mdnow carry Golden Rule 7 plus a## Branchingsection documenting task-branch naming, squash-merge policy, and the release-tag shape.delete_branch_on_merge: trueenabled on origin so merged branches auto-cleanup.
Changed #
fluttersdk_artisanconstraint bumped^0.0.4->^0.0.6. Consumers were already pulling 0.0.6 transitively (via the post-installfluttersdk_artisan: anyline the telescope:install bootstrap appends to the consumer pubspec); telescope's own dev resolution now tracks the same version so tests, format, andpub publish --dry-runrun against the artisan that consumers actually execute. Picks up the 0.0.5 + 0.0.6 fixes:_plugins.g.dartAOT staleness detection, MCPserverInfo.versionsync to0.0.6, atomic.mcp.jsonwrites via.tmp+ rename, themcp:install --invocationplugin-aware fallback, and thedusk_evaluateVM-routed fix. Future artisan 0.0.7 will need a coordinated bump.telescope_*MCP tool descriptions now state the actual wire shape ("oldest-first; last entry is newest"). Previously seven of the eight read tools claimed "Returns newest-first" while the handler delivered oldest-first; the SKILL.md Law 5 disclaimer ("presenter shorthand") that papered over the gap has been retired. Clients reading the description verbatim no longer assume a reversed order.mcp:installfallback now writesdart run fluttersdk_telescope mcp:servewhenbin/fsais absent (via the wrapper's--invocationpass-through to artisan'smcp:install, gated on the 0.0.6 trim-whitespace behavior).telescope:installno longer depends on the AOT-compiledbin/fsa. The chained subprocess calls (install+plugin:install fluttersdk_telescope) now spawndart run fluttersdk_telescope ...directly through the telescope CLI wrapper, mirroring the Cat C subprocess pattern landed influttersdk_dusk. Consumers on a clean checkout (where fsa has not been compiled yet) can complete the bootstrap chain without aProcessException: No such file or directoryfailure. Behavior delta: even consumers withbin/fsascaffolded now invokeplugin:installthroughdart run(a few seconds slower than the fsa AOT proxy on a singletelescope:installinvocation). RequiresdartonPATH(always true on a Flutter dev box). Matches dusk's unconditionaldart runpattern for cross-plugin consistency.
Fixed #
telescope_clearMCP descriptor claimed it cleared "three ring buffers (http, logs, exceptions)" but the implementation has always wiped all 9 buffers atomically (per Core Law 6). Rewrote the description and Usage bullets inlib/src/telescope_artisan_provider.dartto enumerate the 9 buffers (http, logs, exceptions, events, gates, dumps, queries, caches, magic models), document the{"cleared": true}envelope, and make the upstream-sink isolation (Sentry, Bugsnag still receive events) explicit. The wire behavior was already correct; this is a descriptor-string fix only.bin/fluttersdk_telescope.dartnow forcescollectMcpTools: truewhen dispatchingmcp:serve, sodart run fluttersdk_telescope mcp:servesurfaces all 9telescope_*MCP tools. Previously returned 0 plugin tools.
0.0.2 - 2026-05-22 #
Fixed #
- CHANGELOG correction. The
0.0.1archive on pub.dev shipped with a populated[Unreleased]block left over from release prep: every entry listed there (magic dev-dep drop,pubspec_overrides.yamlremoval,test/src/magic/deletion,magictag cleanup indart_test.yaml+ CI workflows + agent-instruction files,example_magic/removal, sub-barrel import path swap intelescope:install) actually shipped INSIDE0.0.1; nothing was published before it. This0.0.2republishes the corrected CHANGELOG so the consolidated0.0.1history surfaces on pub.dev. - README pinned-install snippet bumped from
^0.0.1to^0.0.2so the example matches the published version.
Unchanged #
- No code, no test, no runtime behavior, no public API surface changed.
lib/,bin/,test/,example/, and all 11ext.telescope.*VM Service extensions plus 9telescope_*MCP tools are byte-identical to0.0.1.
0.0.1 - 2026-05-22 #
Initial public release of fluttersdk_telescope. Passive runtime inspector for Flutter apps with a framework-agnostic core and optional Magic-stack integration. Plugin of fluttersdk_artisan ^0.0.4 (hosted-only; no path overrides). Vanilla-Flutter clean: zero magic references in the production or default-test surface. Magic-stack integration is opt-in via runtime detection in telescope:install, which injects import 'package:magic/telescope_integration.dart'; and an if (kDebugMode) MagicTelescopeIntegration.install(); block after await Magic.init( when the consumer's pubspec lists magic:.
Watchers #
9 watchers across vanilla Flutter and Magic-stack:
LogWatcher(auto-installed):package:loggingLogger calls captured to thelogsring buffer.ExceptionWatcher:FlutterError.onError+PlatformDispatcher.instance.onError, chain-preserve previous handlers.DumpWatcher:debugPrintcapture (vanilla Flutter); debug-only.MagicHttpFacadeAdapter: MagicHttpfacade interceptor.MagicModelWatcher:ModelCreated/ModelSaved/ModelDeletedevents from Magic.MagicCacheWatcher:CacheHit/CacheMiss/CachePut/CacheForget/CacheFlushevents.MagicEventWatcher: curated event subscription (auth, db connection, gate-define).MagicGateWatcher:GateAccessCheckedevent after everyGate.allows/Gate.denies.MagicQueryWatcher:QueryExecutedevent from the magic database connector.
Records #
9 immutable record types: HttpRequestRecord, LogRecordEntry, ExceptionRecord, MagicModelRecord, MagicCacheRecord, EventRecord, GateRecord, DumpRecord, QueryRecord.
9-buffer TelescopeStore #
Per-buffer Queue
VM Service extensions (11) #
ext.telescope.requests, .console, .exceptions, .events, .gates, .dumps, .queries, .caches, .clear, .pause, .resume. Every registration goes through registerExtensionIdempotent (from fluttersdk_artisan) for hot-restart safety.
MCP tools (9) #
telescope_tail, telescope_requests, telescope_exceptions, telescope_clear, telescope_events, telescope_gates, telescope_dumps, telescope_queries, telescope_caches. Each is a McpToolDescriptor const instance contributed via TelescopeArtisanProvider.mcpTools().
CLI commands (6) #
telescope:install, telescope:tail, telescope:requests, telescope:queries, telescope:caches, telescope:clear. telescope:install is a one-shot bootstrap that scaffolds the consumer artisan harness, runs plugin:install fluttersdk_telescope, and injects TelescopePlugin.install() into lib/main.dart (Magic-stack anchor or vanilla runApp anchor).
Three public contracts #
TelescopeWatcher:namegetter +install()+uninstall().TelescopeHttpAdapter: same 3-method shape + optionalpendingCountgetter (default 0).McpToolDescriptor: const-constructible; shape owned byfluttersdk_artisan.
TelescopeStore extension surface #
pendingHttpCountgetter sumsTelescopeHttpAdapter.pendingCountacross every registered adapter; consumed byext.dusk.wait_for_network_idlefor network-idle detection.
CI + automated publishing #
.github/workflows/ci.yml: format + analyze + flutter test (--exclude-tags integration) + 80% line-coverage floor (lcov + awk gate) + codecov upload + dart pub publish --dry-run..github/workflows/publish.yml: SemVer tag push triggers validate -> pub.dev publish via the officialdart-lang/setup-dart/.github/workflows/publish.yml@v1reusable workflow with OIDC authentication + github-release job auto-extracting CHANGELOG entry..github/dependabot.yml: weekly pub root + weekly github-actions bumps.
Documentation #
README.mdtwo-path Quick Start (one-shot self-bootstrap viadart run fluttersdk_telescope telescope:install; manual wiring for consumers who prefer to drive the artisan dispatcher by hand). After install, the consumer's./bin/fsanative AOT launcher is the recommended entry point for every subsequent telescope command.doc/tree:getting-started/,watchers/,mcp/.llms.txtat repo root per llmstxt.org spec.skills/fluttersdk-telescope/LLM-agent skill (SKILL.md + 2 references).
Compatibility #
- Dart SDK >=3.4.0 <4.0.0; Flutter >=3.22.0.
- Platforms: Android, iOS, macOS, Linux, Windows, Web (debug-only on every platform; release builds tree-shake the entire telescope subsystem via
kDebugModegate). - Magic-stack integration optional. Vanilla Flutter consumers use Dio adapter + LogWatcher + ExceptionWatcher + DumpWatcher with no Magic dependency.
Test coverage #
249 tests green at release time across watchers, records, commands, extensions, and the artisan provider. 80% line coverage floor enforced in CI (current measured coverage 95.60%).